From fe0471dc522726e6588a370535c1efe93ad699ed Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 18:45:51 -0500 Subject: [PATCH 1/8] docs(desktop): design the minimal native desktop app (HACK-2214) Views and states, onboarding with terminal hand-off for admin steps, doctor cards, upgrades, daemon and always-on states, the CLI/daemon data contract with numbered requests, scope lines and distribution. Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 301 ++++++++++++++++++++++++ apps/desktop/diagrams/admin-handoff.mmd | 18 ++ apps/desktop/diagrams/admin-handoff.svg | 66 ++++++ apps/desktop/diagrams/always-on.mmd | 23 ++ apps/desktop/diagrams/always-on.svg | 66 ++++++ apps/desktop/diagrams/data-flow.mmd | 15 ++ apps/desktop/diagrams/data-flow.svg | 66 ++++++ apps/desktop/diagrams/onboarding.mmd | 26 ++ apps/desktop/diagrams/onboarding.svg | 66 ++++++ apps/desktop/diagrams/project-state.mmd | 28 +++ apps/desktop/diagrams/project-state.svg | 66 ++++++ apps/desktop/diagrams/upgrade.mmd | 27 +++ apps/desktop/diagrams/upgrade.svg | 66 ++++++ 13 files changed, 834 insertions(+) create mode 100644 apps/desktop/DESIGN.md create mode 100644 apps/desktop/diagrams/admin-handoff.mmd create mode 100644 apps/desktop/diagrams/admin-handoff.svg create mode 100644 apps/desktop/diagrams/always-on.mmd create mode 100644 apps/desktop/diagrams/always-on.svg create mode 100644 apps/desktop/diagrams/data-flow.mmd create mode 100644 apps/desktop/diagrams/data-flow.svg create mode 100644 apps/desktop/diagrams/onboarding.mmd create mode 100644 apps/desktop/diagrams/onboarding.svg create mode 100644 apps/desktop/diagrams/project-state.mmd create mode 100644 apps/desktop/diagrams/project-state.svg create mode 100644 apps/desktop/diagrams/upgrade.mmd create mode 100644 apps/desktop/diagrams/upgrade.svg diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md new file mode 100644 index 000000000..995d596e0 --- /dev/null +++ b/apps/desktop/DESIGN.md @@ -0,0 +1,301 @@ +# Hack desktop app: design (v0) + +Status: proposal (HACK-2214). Optional, unsupported until the lead says otherwise. +Not part of the default gates, the release workflow or onboarding docs. + +One small native window that shows every Hack project, what it is doing and what +is wrong, and runs `hack` for you. It is a view over the CLI and `hackd`, not a +second implementation of them. + +![Data flow](diagrams/data-flow.svg) ([source](diagrams/data-flow.mmd)) + +## Principles + +| Rule | Consequence | +|---|---| +| The CLI is the only writer | Every action is `hack --json --no-interactive` run as the user, with `--path ` or `--project `. | +| No app state beyond window layout | Selected project, window size, sidebar width, theme override. Nothing else is stored. | +| No credentials, no passwords | The app never shows a password field and never runs `sudo`. Admin steps go to a terminal (see [Onboarding](#2-first-run-onboarding)). | +| No secret values | Env shows names and layers only. Log lines are shown as the CLI emits them, with a warning (see [Logs](#logs-tab)). | +| Unknown is not absent | A failed or timed-out read shows "could not read" with the error, never "stopped" or "0 projects". | +| Small | Target under 4,000 lines of Rust for v0.1. The old `apps/macos` grew to 13,800 lines through an embedded terminal, command palette, editor and agent integrations, tickets and remote features. None of those come back. | + +Toolkit: Rust, `gpui-kit = "=0.9.1"` (GPUI through `gpui-pre =0.3.12` from crates.io), +edition 2024, Rust 1.92+ (the repo pins 1.97.1). Its own Cargo workspace in +`apps/desktop/`: `core` (no GUI dependency) and `app`. + +## 1. Views and states + +Window: sidebar (projects, then machine rows "Routing and DNS" and "Doctor"), +detail pane, status bar. Light and dark follow the system +(`Theme::sync_system_appearance` plus an appearance observer). + +### Project list (sidebar) + +| State | Trigger | Shown | Action | +|---|---|---|---| +| Loading | First read in flight (< 2 s) | Three skeleton rows | none | +| Loaded | `projects[]` non-empty | Groups Running / Stopped; dot per row (green running, amber degraded, grey stopped, red failed or needs attention); pin icon when always-on | Select row | +| Empty | Registry has no projects | "No projects yet" + `cd your-repo && hack init` (copy) | Copy command, open docs | +| Daemon not running | `/v1/status` unreachable | List still loads through the CLI fallback; thin banner "hackd is off: status refreshes slowly" | Start hackd | +| Daemon other version | `/v1/status.version` ≠ CLI version | Same fallback; banner "hackd vX, CLI vY" | Restart hackd | +| Engine not running | `runtime_ok: false` | Rows keep last known names, dots grey with "?"; banner with `runtime_error` | Open engine help | +| CLI missing or too old | `hack` not on PATH, or below the supported range | Full-pane card instead of the list | Copy install command | +| Error | CLI exit ≠ 0 or bad JSON | Banner with error `code` and `message`; last good list kept and marked stale with its age | Retry, copy error | + +Machine rows: "Routing and DNS" (dot from `hack global status --json` +`summary.ok`), "Doctor" (badge = count of `warn` + `error`, red if any `error`). + +### Project detail: header per state + +Header: name, state pill (+ uptime), backend pill (`compose` / `native`), URL, +buttons, "Always on" switch. Tabs: Services · Logs · Env · Activity. + +![Project states](diagrams/project-state.svg) ([source](diagrams/project-state.mmd)) + +| State | Derived from (until request R2 lands) | Pill | Buttons | +|---|---|---|---| +| Stopped | `status: stopped` | grey "Stopped" | **Start**, Open (disabled) | +| Starting | app's own `up` in flight, or native `phase: pending` | blue spinner "Starting…" + service progress when R6 exists | Stop (cancels) | +| Running | all `defined_services` have a running container | green "Running · 2h 14m" | Open, Restart, Stop | +| Degraded | some running, some defined service not running or unhealthy | amber "Degraded · api down" | Open, Restart, Stop | +| Stopping | app's own `down` in flight | grey spinner "Stopping…" | none | +| Failed start | last `up` envelope has `services.failed`, or a container exited ≠ 0 after start | red "Failed to start: api, worker" | Retry, Logs (filtered to failed services), Stop | +| Needs attention | always-on gave up (R12), `status: missing`, or `input_diagnostic` present | red outline "Needs attention" + one line why | the one fix (Retry / Open folder / Run doctor) | +| Unknown | runtime unreadable | grey "?" "Status unknown" + error | Retry | + +Actions run one at a time per project; buttons disable while one runs. A finished +action re-reads the project at once instead of waiting for the next poll. + +### Services tab + +| State | Shown | +|---|---| +| Running / degraded | Table: service, state (dot + text), CPU %, memory, route (host link). Down services sort first. Short log tail (last 20 lines, all services) below. | +| Stopped | Table of `defined_services` with "—" for CPU and memory; no tail. | +| Usage unavailable | CPU and memory show "—" with a tooltip ("usage needs R3"). | +| Failed start | Failed rows red with exit code; tail is filtered to those services. | + +### Logs tab + +Source: `hack logs --json --path [--services a,b] --tail 500` (ndjson, follow by default). + +| State / control | Behaviour | +|---|---| +| Follow (default) | Newest at bottom; auto-scroll while at the bottom. | +| Pause | Stops auto-scroll; keeps reading into a 10,000-line ring buffer; shows "N new lines" chip. Scrolling up pauses. | +| Search | Filters the buffer in place (plain text, case-insensitive); shows match count. | +| Service filter | Chips per service; changing it restarts the stream with `--services`. | +| Very long lines | Cut at 4 KB on screen with "… (+N KB)"; click to expand; `entry.truncated` shown as "truncated by hack". | +| Secrets warning | Fixed one-line note: "Logs are shown as your services print them. Hack does not hide secrets in logs." No copy-all button in v0. | +| Stream ended / error | `end` or `error` event: banner with reason and Reconnect. Engine down: "Engine not running". | +| Stopped project | Last 200 lines (`--no-follow`), marked "project stopped". | + +### Env tab (names only, never values) + +Source: `hack env list --json --path `. + +| State | Shown | +|---|---| +| Normal | Table: name, layer (`layer` / `resolved_from`), scope, kind (secret or plain from `storage.kind`), set (`has_value` ✓/✗), required. | +| Missing required | Red rows from `missing_required`, top of table. | +| Locked | Key missing or unreadable: lock icon, "Env key not available: names are shown, values cannot be resolved", one action (copy `hack doctor` command). Needs R10 to detect reliably. | +| No env config | "This project has no env layers" + docs link. | + +Editing is out of scope until "later". Never a "reveal" button. + +### Activity tab + +Needs R5. Until then the tab shows only actions this app ran in this session (kept +in memory, gone on quit) with the note "Full history needs a newer hack". + +| Row kind | Fields | +|---|---| +| Start / stop / restart | time, who (`cli` / `app` / `autostart` / `agent`), duration, result | +| Failure | time, failed services, error code, link to logs at that time | +| Always-on | paused after manual stop, resumed, gave up after N tries | + +### Status bar + +Left: "3 running · 5 stopped". Middle: engine state (Docker or native, ok/down), +hackd (on / off / version mismatch). Right: CLI version, "Update available" chip. + +## 2. First-run onboarding + +Shown on first launch and from the Doctor row ("Run setup again"). One check per +step, in order; each step is a card with status, why it matters, exact command. + +![Onboarding](diagrams/onboarding.svg) ([source](diagrams/onboarding.mmd)) + +| Step | Check (read-only) | Fix command | Admin | Skipped shows | +|---|---|---|---|---| +| CLI | `hack version --json` (R1) | `brew install hack-dance/tap/hack` | no (app only copies it) | Cannot skip: nothing works without it | +| Engine | `runtime_ok` from `/v1/projects` or `hack projects --json` | open Docker Desktop / docs; native: `hack native setup` style command (HACK-2193) | no | Banner on every project: "Engine not running" | +| hackd | `hack daemon status --json` | `hack daemon start` | no | Status bar "hackd off"; slower refresh | +| Routing and DNS | `hack global status --json` `summary.host_dns_ok`, `caddy_ok` | `hack global install` | **yes** (resolver files, dnsmasq or `resolvectl`) | URLs shown greyed with "DNS not set up"; Open uses `localhost` ports if known | +| Certificate trust | doctor check `caddy local ca` | `hack global trust` | **yes** (System keychain / system trust store) | Lock icon on URLs: "browser will warn" | +| Login service | `daemon status` `service.installed` | `hack daemon install` | no | Always-on switch disabled: "needs the login service" | + +Every step: **Run** (non-admin, the app runs the command and streams its output), +**Open in Terminal** (admin), **Copy command**, **Skip**, **Check again**. Skips +are not remembered as decisions: the next check simply finds the step still open +and Doctor lists it. There is no hidden "don't ask again" state. + +### Admin steps: system prompt vs terminal hand-off + +![Admin hand-off](diagrams/admin-handoff.svg) ([source](diagrams/admin-handoff.mmd)) + +| | System prompt (macOS `do shell script … with administrator privileges`, Linux `pkexec`) | Terminal hand-off | +|---|---|---| +| What runs as root | A command the app built | The CLI's own command; the CLI calls `sudo` only where it needs it | +| Mixed user/root steps | Breaks: running `hack global trust` as root writes root-owned files into the user's Hack home | Works: the CLI already splits user and root steps | +| User can read what runs | Only what the app shows | Yes, typed in the terminal before Enter | +| Linux availability | `pkexec` needs a running polkit agent; varies by desktop | Any terminal (`xdg-terminal-exec`, `$TERMINAL`, fallbacks) | +| Polish | Native dialog, Touch ID | One extra window | + +Recommendation: **terminal hand-off on both platforms for v0.** The app writes +nothing executable: it opens the terminal with the command typed and waiting for +Enter, then re-runs the check when the terminal process exits (or on "Check +again"). macOS system prompt can come later, only for single root-only commands +the CLI exposes as an argv plan (R11), never for whole `hack` commands. + +## 3. Doctor + +Source: `hack doctor --json` (today: `checks[{id,status,detail}]`), on open of the +Doctor row and every 10 minutes while the window is visible. + +| Card part | Content | +|---|---| +| Group | Area: Engine, Routing and DNS, Certificates, Daemon, Env and secrets, Project files, Native | +| Severity | `error` red, `warn` amber; `ok` collapsed into "12 checks passed" | +| Affected | "Machine" or the project names it applies to | +| One action | Run safe fix (non-admin, `hack doctor --fix --only `, R7) · Open in Terminal (admin) · Copy command · Open doc | + +Machine-wide findings appear once under their area, never repeated on each +project. A project's own findings show as a "Needs attention" line in its header +that links to the Doctor card. Until R7 adds `area`, `scope` and `fix`, the app maps +known check `id`s to areas with a small table in `core` and shows unknown ids under +"Other" with only Copy and Open doc. + +## 4. Upgrades + +![Upgrade](diagrams/upgrade.svg) ([source](diagrams/upgrade.mmd)) + +| State | Source | Shown | Action | +|---|---|---|---| +| Up to date | `hack update --check --json` `updateAvailable: false` (daily) | nothing | none | +| Available | `updateAvailable: true`, `latest` | Status bar chip "Hack vX available" | Review | +| Checking | `hack upgrade check --json` (#790, read-only) | Spinner, then plan | none | +| Blocked | `ready: false`, `blockers[]` | One card per blocker with its `command` or `next_action` | Run / Copy, Re-check | +| Incomplete | `incomplete[]` | "Could not check: …" cards; treated like Blocked | Re-check | +| Ready | `ready: true`, `plan[]` | Ordered plan list (machine steps, then projects) | Upgrade | +| Snapshotting | `hack upgrade snapshot --json` | Progress; manifest path kept for rollback | none | +| Applying | Homebrew install: Terminal `brew upgrade hack` (formula and cask); then plan steps | Step list with ✓/✗ | none | +| Restarting daemon | `hack daemon restart` (HACK-2216), then `/v1/status.version` == CLI version | "Restarting hackd…" | none | +| Failed | any step exit ≠ 0, or versions still differ | Which step, its error, and "Your files were saved at snapshot …" | Roll back, Copy error | +| Rolling back | `hack upgrade rollback --rollback-manifest --json` | Restored / could not restore list (`keptAs`, reasons) | none | + +The app never upgrades itself separately: the cask and formula move together, and +the app restarts itself only after the user confirms ("Relaunch to finish"). + +## 5. Daemon + +Source: `hack daemon status --json` + `GET /v1/status` (version). + +| State | Detected by | Shown | One action | +|---|---|---|---| +| Not installed, not running | `running: false`, `service.installed: false` | Status bar "hackd off" | Start (`hack daemon start`); setup offers `hack daemon install` | +| Installed, stopped | `service.installed: true`, `running: false` | "hackd stopped (login service installed)" | Start (`hack daemon start`, goes through the service manager) | +| Running, other version | `/v1/status.version` ≠ `hack version` | "hackd vX, CLI vY" amber | Restart (`hack daemon restart`, HACK-2216) | +| Stale | `stale: true` (`stale_reason`), or unit points at a missing or other binary (R8) | "hackd unit is stale" + `next_step` | Repair (`hack daemon install`), else Copy `next_step` | +| Incompatible / starting | `status` | as given, with `issue` | Wait / Restart | +| Running | ok | green dot only | none | + +The app never kills a daemon process or edits a unit itself. + +## 6. Always-on (HACK-2213) + +![Always-on](diagrams/always-on.svg) ([source](diagrams/always-on.mmd)) + +| State | Shown on the project | Action | +|---|---|---| +| Off | Switch off | Switch on → `hack autostart add --project ` | +| On | Switch on, pin in sidebar | Switch off → `hack autostart remove --project ` | +| Paused until you start it | After a manual Stop: switch on, label "Paused until you start it" | Start (resumes) | +| Needs attention | After N failed starts: red label "Always-on stopped trying after 3 failures" + failed services | Retry (`hack autostart resume --project`), Logs, Turn off | +| Globally paused | Banner "Always-on paused for all projects" + menu item | Resume all (`hack autostart resume`) | +| At login | While the login run is in progress: sidebar header "Starting 2 of 4…", each pending row in Starting | none; failures become Needs attention | +| No login service | Switch disabled, tooltip "needs `hack daemon install`" | Install login service | + +Fields and commands the app needs from the CLI side (design contract for HACK-2213): + +| Where | Field / command | +|---|---| +| per project in `hack status --json` / `/v1/projects` | `autostart.enabled: bool`, `autostart.state: "off" \| "on" \| "paused" \| "starting" \| "needs_attention"`, `autostart.paused_reason: "manual_stop" \| null`, `autostart.failures.count`, `autostart.failures.last_at`, `autostart.failures.failed_services[]`, `autostart.failures.last_error_code`, `autostart.max_failures` | +| global in the same payload | `autostart_global.paused: bool`, `autostart_global.login_run: { in_progress, started_at, pending[], started[], failed[] }`, `autostart_global.service_ready: bool` | +| commands (all `--json --no-interactive`) | `hack autostart add\|remove --project `, `hack autostart list --json`, `hack autostart pause [--project ]`, `hack autostart resume [--project ]` (resume also clears the failure count) | +| behaviour | `hack down` on an always-on project sets `paused/manual_stop`; `hack up` clears it. The app relies on that, not on its own bookkeeping. | + +## 7. Data contract + +Polling stops when the window is hidden (menu bar popover later polls only while +open). Every CLI call has a timeout and is killed with its process group on expiry. + +| Screen | Reads | How often | Timeout | +|---|---|---|---| +| Sidebar, header | `GET /v1/projects?summary=1` if `/v1/status.version` matches, else `hack projects --json --summary` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | +| Detail (selected) | `GET /v1/projects?filter=` (else `hack projects --json`), plus `hack ps --json --path` for native | 2 s while selected | 3 s / 15 s | +| Services CPU/memory | `hack usage --json --details` (until R3) | 5 s while Services is visible | 10 s | +| Logs | `hack logs --json --path --tail 500` (one long-running child) | stream | none; killed on tab change | +| Env | `hack env list --json --path ` | on tab open, after env actions | 10 s | +| Activity | `hack activity --json --project --limit 100` (R5) | on tab open, 10 s while open | 10 s | +| Doctor, onboarding | `hack doctor --json`, `hack global status --json`, `hack daemon status --json` | on open; every 10 min | 60 s | +| Daemon | `GET /v1/status` | every 10 s | 1 s | +| Upgrades | `hack update --check --json` daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | +| Actions | `hack up -d`, `down`, `restart`, `open --json`, `daemon start\|restart\|install`, `autostart …` | on click | 10 min for `up`, 2 min others | + +### Requests to the CLI and daemon (one PR each, for other sessions) + +1. **R1 `hack version --json`**: `{version, channel, install_kind, binary_path}`, so the app can check the supported range and say how to upgrade. +2. **R2 project `state` field** in `/v1/projects` and `hack projects --json`: `running | degraded | starting | stopping | stopped | failed | unknown`, plus `started_at` and `failed_services[]`. Moves state derivation out of the app. +3. **R3 per-service usage**: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes` per service. +4. **R4 log stream guarantees**: document `hack logs --json` ndjson as a stable contract and add `--max-line-bytes ` so the CLI sets `entry.truncated`. +5. **R5 activity history**: append-only record written by `up`, `down`, `restart` and autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), read with `hack activity --json [--project] [--limit]`. +6. **R6 action progress**: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` lines before the final envelope. +7. **R7 structured doctor findings**: add `area`, `scope: machine | project`, `projects[]`, `fix: {kind: safe | admin | manual, args[], doc_url}` per check (same shape as `UpgradeFinding` in #790), and `hack doctor --fix --only --yes --json` for one safe fix without prompts. +8. **R8 daemon status version and unit staleness**: `version` and `cli_version` in `hack daemon status --json`, plus `service.unit_stale` and its reason. +9. **R9 push updates (optional)**: `GET /v1/projects?wait=` long-poll, so the app can stop 2-second polling. +10. **R10 env key state**: top-level `key_state: ok | missing | unreadable` in `hack env list --json` (no values). +11. **R11 admin argv plan**: `hack global install|trust --plan --json` listing the exact root-only argv lines, so the app can show them and later hand single lines to the macOS system prompt. +12. **R12 always-on fields and commands** as listed in [section 6](#6-always-on-hack-2213). + +## 8. Scope + +| Version | Contains | +|---|---| +| v0 | Window, sidebar, detail header, Services table, log tail, status bar, light/dark; Start, Stop, Restart, Open, follow logs; `--fixtures ` mode. | +| v0.1 | Logs tab, Doctor view, onboarding, Env tab (names), always-on and daemon states, upgrade flow; Homebrew cask; Linux tarball. | +| Later | Menu bar popover (open/start/stop, "Pause always-on"); activity charts; env editing through `hack env set`; branch instances; resource graphs. | +| Never | Edit Compose files or Hack config; show, copy or export secret values; install, start or manage Docker itself; ask for or store a password; an embedded terminal, editor or agent; remote/gateway/tickets; its own database. | + +Menu bar: gpui-kit 0.9.1 and GPUI have no tray or status item API (only the app +menu, dock menu and in-window menus). The popover needs a separate crate such as +`tray-icon`, not yet checked against GPUI's event loop. That is why it is "later". + +## 9. Distribution + +| Item | Plan | +|---|---| +| macOS | Cask `hack-desktop` in `hack-dance/homebrew-tap` beside `Formula/hack.rb`, `depends_on formula: "hack"`; `brew install --cask hack-desktop`. The formula offers it in caveats only; the CLI install never pulls the app. | +| Signing | Developer ID Application certificate, hardened runtime, no App Sandbox (the app runs `hack` and reads the hackd socket). Notarize with `notarytool submit --wait`, then `stapler staple`. Certificate, its password and the notary API key live only in the owner's password manager and CI secrets; this design sets none of them up. | +| Release job | A separate, manual workflow later builds the universal `.app`, signs, notarizes, zips, and updates the cask with its sha256. It does not join the CLI release until the lead decides. | +| Auto-update | None. Homebrew owns updates (`brew upgrade`), and the app only shows "update available". gpui-kit ships no updater. | +| Linux | v0.1: tarball with binary, `.desktop` file and icon, plus an `install.sh` into `~/.local`. Later maybe `.deb`. No Flatpak/Snap: their sandboxes block running `hack` and reading the socket. | +| Minimums | macOS 15+. Linux: Wayland or X11 with Vulkan (software Vulkan works), glibc of Ubuntu 24.04 or newer. Windows: not planned. | +| Version match | The app reads `hack version --json` and supports a declared CLI range; outside it, it shows only the "update Hack" card. | + +## Open questions for the lead + +1. Cask name `hack-desktop` and install opt-in only (recommended), or offered by `hack setup`? +2. Who runs always-on at login: hackd at service start (recommended) or a separate unit? +3. Is terminal hand-off acceptable for v0 admin steps on macOS (recommended), or is the system prompt a v0 requirement? diff --git a/apps/desktop/diagrams/admin-handoff.mmd b/apps/desktop/diagrams/admin-handoff.mmd new file mode 100644 index 000000000..50d26d60c --- /dev/null +++ b/apps/desktop/diagrams/admin-handoff.mmd @@ -0,0 +1,18 @@ +--- +title: Admin step · terminal hand-off +--- +sequenceDiagram + autonumber + actor U as User + participant A as App + participant T as Terminal + participant C as hack CLI + A->>U: Explain step, show exact command + U->>A: Open in Terminal + A->>T: New window, command typed, waits for Enter + U->>T: Enter + T->>C: hack global trust + C->>U: sudo asks for password (in Terminal) + C-->>T: exit code + A->>C: re-run the check (hack doctor --json) + A->>U: Step done, failed, or skipped diff --git a/apps/desktop/diagrams/admin-handoff.svg b/apps/desktop/diagrams/admin-handoff.svg new file mode 100644 index 000000000..2260159a4 --- /dev/null +++ b/apps/desktop/diagrams/admin-handoff.svg @@ -0,0 +1,66 @@ +Admin step · terminal hand-off123456789Explain step, show exact commandOpen in TerminalNew window, command typed, waits for EnterEnterhack global trustsudo asks for password (in Terminal)exit codere-run the check (hack doctor --json)Step done, failed, or skippedUserAppTerminalhack CLIUserAppTerminalhack CLI \ No newline at end of file diff --git a/apps/desktop/diagrams/always-on.mmd b/apps/desktop/diagrams/always-on.mmd new file mode 100644 index 000000000..3cfc8008f --- /dev/null +++ b/apps/desktop/diagrams/always-on.mmd @@ -0,0 +1,23 @@ +--- +title: Always-on · one project +--- +stateDiagram-v2 + direction TB + [*] --> Off + Off --> On : switch on + On --> Off : switch off + On --> Paused : manual Stop + Paused --> On : manual Start + On --> NeedsAttention : 3 failed starts + NeedsAttention --> On : Retry ok + NeedsAttention --> Off : switch off + note right of On + INV global pause holds every + project without changing it + end note + classDef live stroke-width:1.6px + classDef wait stroke-dasharray:2 2 + classDef fail stroke-width:2px + class On live + class Paused wait + class NeedsAttention fail diff --git a/apps/desktop/diagrams/always-on.svg b/apps/desktop/diagrams/always-on.svg new file mode 100644 index 000000000..2fb84e7d2 --- /dev/null +++ b/apps/desktop/diagrams/always-on.svg @@ -0,0 +1,66 @@ +Always-on · one projectswitch onswitch offmanual Stopmanual Start3 failed startsRetry okswitch offOffPausedNeedsAttentionOnINV global pause holds everyproject without changing it \ No newline at end of file diff --git a/apps/desktop/diagrams/data-flow.mmd b/apps/desktop/diagrams/data-flow.mmd new file mode 100644 index 000000000..89822f7e7 --- /dev/null +++ b/apps/desktop/diagrams/data-flow.mmd @@ -0,0 +1,15 @@ +--- +title: Desktop app · where data comes from +--- +flowchart LR + App["Hack desktop app"] -->|"hack … --json (as the user)"| CLI["hack CLI"] + App -->|"GET /v1/status, /v1/projects"| D["hackd socket"] + CLI --> D + CLI --> E["Engine (Docker or native VM)"] + D -->|"events + 30s inspect"| E + App -.->|"opens"| T["Terminal (admin steps)"] + App -.->|"opens"| B["Browser (project URL)"] + classDef live stroke-width:1.6px + classDef idle stroke-dasharray:4 3 + class D live + class T,B idle diff --git a/apps/desktop/diagrams/data-flow.svg b/apps/desktop/diagrams/data-flow.svg new file mode 100644 index 000000000..36aadf201 --- /dev/null +++ b/apps/desktop/diagrams/data-flow.svg @@ -0,0 +1,66 @@ +Desktop app · where data comes fromhack … --json (as the user)GET /v1/status, /v1/projectsevents + 30s inspectopensopensHack desktop apphack CLIhackd socketEngine (Docker or nativeVM)Terminal (admin steps)Browser (project URL) \ No newline at end of file diff --git a/apps/desktop/diagrams/onboarding.mmd b/apps/desktop/diagrams/onboarding.mmd new file mode 100644 index 000000000..0c3bda950 --- /dev/null +++ b/apps/desktop/diagrams/onboarding.mmd @@ -0,0 +1,26 @@ +--- +title: First run · checks in order +--- +flowchart TD + S([App opens]) --> C1{"hack CLI found,
version supported?"} + C1 -->|no| X1["Install CLI card
(brew command)"] --> C1 + C1 -->|yes| C2{"Engine running?"} + C2 -->|no| X2["Engine card:
open Docker / native setup"] + C2 -->|yes| C3{"hackd running?"} + X2 -. skip .-> C3 + C3 -->|no| X3["Start hackd
(no admin)"] + C3 -->|yes| C4{"Routing + DNS ok?"} + X3 -. skip .-> C4 + C4 -->|no| X4["DNS step
needs admin"] + C4 -->|yes| C5{"Certificate trusted?"} + X4 -. skip .-> C5 + C5 -->|no| X5["Trust step
needs admin"] + C5 -->|yes| C6{"Login service?"} + X5 -. skip .-> C6 + C6 -->|no| X6["Install login service
(no admin, optional)"] + C6 -->|yes| Done([Project list]) + X6 -. skip .-> Done + classDef wait stroke-dasharray:2 2 + classDef ok stroke-width:1.6px + class X4,X5 wait + class Done ok diff --git a/apps/desktop/diagrams/onboarding.svg b/apps/desktop/diagrams/onboarding.svg new file mode 100644 index 000000000..1262eede0 --- /dev/null +++ b/apps/desktop/diagrams/onboarding.svg @@ -0,0 +1,66 @@ +First run · checks in ordernoyesnoyesskipnoyesskipnoyesskipnoyesskipnoyesskipApp openshack CLI found,version supported?Install CLI card(brew command)Engine running?Engine card:open Docker / native setuphackd running?Start hackd(no admin)Routing + DNS ok?DNS stepneeds adminCertificate trusted?Trust stepneeds adminLogin service?Install login service(no admin, optional)Project list \ No newline at end of file diff --git a/apps/desktop/diagrams/project-state.mmd b/apps/desktop/diagrams/project-state.mmd new file mode 100644 index 000000000..a6fba4839 --- /dev/null +++ b/apps/desktop/diagrams/project-state.mmd @@ -0,0 +1,28 @@ +--- +title: Project detail · display states +--- +stateDiagram-v2 + direction TB + [*] --> Stopped + Stopped --> Starting : Start + Starting --> Running : all services up + Starting --> FailedStart : a service exited + Running --> Degraded : a service down + Degraded --> Running : service back + Running --> Stopping : Stop + Degraded --> Stopping : Stop + Stopping --> Stopped : down done + FailedStart --> Starting : Retry + FailedStart --> Stopped : Stop + FailedStart --> NeedsAttention : always-on 3x + NeedsAttention --> Starting : Retry + note right of NeedsAttention + INV the app never retries + by itself + end note + classDef live stroke-width:1.6px + classDef wait stroke-dasharray:2 2 + classDef fail stroke-width:2px + class Running live + class Starting,Stopping wait + class FailedStart,NeedsAttention,Degraded fail diff --git a/apps/desktop/diagrams/project-state.svg b/apps/desktop/diagrams/project-state.svg new file mode 100644 index 000000000..943f290d6 --- /dev/null +++ b/apps/desktop/diagrams/project-state.svg @@ -0,0 +1,66 @@ +Project detail · display statesStartall services upa service exiteda service downservice backStopStopdown doneRetryStopalways-on 3xRetryStoppedStartingRunningFailedStartDegradedStoppingNeedsAttentionINV the app never retriesby itself \ No newline at end of file diff --git a/apps/desktop/diagrams/upgrade.mmd b/apps/desktop/diagrams/upgrade.mmd new file mode 100644 index 000000000..7c108ee23 --- /dev/null +++ b/apps/desktop/diagrams/upgrade.mmd @@ -0,0 +1,27 @@ +--- +title: Upgrade · app view of the flow +--- +stateDiagram-v2 + direction TB + [*] --> UpToDate + UpToDate --> Available : update --check + Available --> Checking : Review + Checking --> Blocked : blockers + Blocked --> Checking : Re-check + Checking --> Ready : ready true + Ready --> Snapshotting : Upgrade + Snapshotting --> Applying : snapshot ok + Snapshotting --> Failed : snapshot refused + Applying --> RestartingDaemon : apply ok + Applying --> Failed : step failed + RestartingDaemon --> UpToDate : versions match + RestartingDaemon --> Failed : version mismatch + Failed --> RollingBack : Roll back + RollingBack --> UpToDate : restored + RollingBack --> Failed : partial restore + classDef live stroke-width:1.6px + classDef wait stroke-dasharray:2 2 + classDef fail stroke-width:2px + class Snapshotting,Applying,RestartingDaemon,RollingBack wait + class Blocked,Failed fail + class UpToDate live diff --git a/apps/desktop/diagrams/upgrade.svg b/apps/desktop/diagrams/upgrade.svg new file mode 100644 index 000000000..b18755ed4 --- /dev/null +++ b/apps/desktop/diagrams/upgrade.svg @@ -0,0 +1,66 @@ +Upgrade · app view of the flowupdate --checkReviewblockersRe-checkready trueUpgradesnapshot oksnapshot refusedapply okstep failedversions matchversion mismatchRoll backrestoredpartial restoreUpToDateAvailableCheckingBlockedReadySnapshottingApplyingFailedRestartingDaemonRollingBack \ No newline at end of file From f4881106a55541df20f17912de4148a96129791a Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:05:25 -0500 Subject: [PATCH 2/8] docs(desktop): address design review (observe-only reads, native mapping, upgrade and daemon contracts) Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 355 ++++++++++++++---------- apps/desktop/diagrams/admin-handoff.mmd | 22 +- apps/desktop/diagrams/admin-handoff.svg | 4 +- apps/desktop/diagrams/always-on.mmd | 11 +- apps/desktop/diagrams/always-on.svg | 4 +- apps/desktop/diagrams/onboarding.mmd | 34 +-- apps/desktop/diagrams/onboarding.svg | 4 +- apps/desktop/diagrams/project-state.mmd | 38 +-- apps/desktop/diagrams/project-state.svg | 4 +- apps/desktop/diagrams/upgrade.mmd | 30 +- apps/desktop/diagrams/upgrade.svg | 4 +- 11 files changed, 285 insertions(+), 225 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 995d596e0..3a8b39e38 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -13,10 +13,11 @@ second implementation of them. | Rule | Consequence | |---|---| -| The CLI is the only writer | Every action is `hack --json --no-interactive` run as the user, with `--path ` or `--project `. | +| The CLI is the only writer | Every action is one `hack --no-interactive` run as the user with `--path `. Commands without `--json` are judged by exit code, then a fresh read. | +| Reads observe only | A poll never starts Docker, hackd, a VM or anything else. A command that can start something (today: `hack global status`) is not polled until it has an observe-only mode (R13). | | No app state beyond window layout | Selected project, window size, sidebar width, theme override. Nothing else is stored. | -| No credentials, no passwords | The app never shows a password field and never runs `sudo`. Admin steps go to a terminal (see [Onboarding](#2-first-run-onboarding)). | -| No secret values | Env shows names and layers only. Log lines are shown as the CLI emits them, with a warning (see [Logs](#logs-tab)). | +| No credentials, no passwords | No password field, no `sudo` from the app. Admin steps go to the user's terminal ([Onboarding](#2-first-run-onboarding)). | +| Hack's secret values never appear | Env shows names and layers only, never a reveal. Logs are service output, shown as the CLI emits them, with a warning, never written to disk and with no copy-all (owner decision: the Logs tab is in scope). | | Unknown is not absent | A failed or timed-out read shows "could not read" with the error, never "stopped" or "0 projects". | | Small | Target under 4,000 lines of Rust for v0.1. The old `apps/macos` grew to 13,800 lines through an embedded terminal, command palette, editor and agent integrations, tickets and remote features. None of those come back. | @@ -24,6 +25,10 @@ Toolkit: Rust, `gpui-kit = "=0.9.1"` (GPUI through `gpui-pre =0.3.12` from crate edition 2024, Rust 1.92+ (the repo pins 1.97.1). Its own Cargo workspace in `apps/desktop/`: `core` (no GUI dependency) and `app`. +Existing contracts are marked as such; `hack upgrade …` is PR #790 (open), +`hack autostart …` (HACK-2213) and the post-upgrade daemon restart (HACK-2216) +are **proposed** contracts this design asks for. + ## 1. Views and states Window: sidebar (projects, then machine rows "Routing and DNS" and "Doctor"), @@ -34,61 +39,84 @@ detail pane, status bar. Light and dark follow the system | State | Trigger | Shown | Action | |---|---|---|---| -| Loading | First read in flight (< 2 s) | Three skeleton rows | none | -| Loaded | `projects[]` non-empty | Groups Running / Stopped; dot per row (green running, amber degraded, grey stopped, red failed or needs attention); pin icon when always-on | Select row | -| Empty | Registry has no projects | "No projects yet" + `cd your-repo && hack init` (copy) | Copy command, open docs | -| Daemon not running | `/v1/status` unreachable | List still loads through the CLI fallback; thin banner "hackd is off: status refreshes slowly" | Start hackd | -| Daemon other version | `/v1/status.version` ≠ CLI version | Same fallback; banner "hackd vX, CLI vY" | Restart hackd | -| Engine not running | `runtime_ok: false` | Rows keep last known names, dots grey with "?"; banner with `runtime_error` | Open engine help | -| CLI missing or too old | `hack` not on PATH, or below the supported range | Full-pane card instead of the list | Copy install command | -| Error | CLI exit ≠ 0 or bad JSON | Banner with error `code` and `message`; last good list kept and marked stale with its age | Retry, copy error | - -Machine rows: "Routing and DNS" (dot from `hack global status --json` -`summary.ok`), "Doctor" (badge = count of `warn` + `error`, red if any `error`). +| Loading | First read in flight | Three skeleton rows | none | +| Loaded | `projects[]` non-empty | Groups Running / Stopped; dot per row (green running, amber degraded, grey stopped, red failed or needs attention, grey "?" unknown); "always on" mark | Select row | +| Empty | Registry has no projects | "No projects yet" + `cd your-repo && hack init` | Copy command, open docs | +| Daemon not running | `/v1/status` unreachable | List loads through the CLI; banner "hackd is off: status refreshes slowly" | Start hackd | +| Daemon other version | `/v1/status.version` ≠ CLI version | CLI fallback; banner "hackd vX, CLI vY" | Restart hackd | +| Engine not running | `runtime_ok: false` | Rows keep names, dots "?"; banner with `runtime_error` | Open engine help | +| CLI missing or too old | `hack` not on PATH, or outside the supported range | Full-pane card instead of the list; status bar shows no engine/hackd state | Copy install command | +| Error | CLI exit ≠ 0 or bad JSON | Banner with `code` and `message`; last good list kept, marked stale with its age | Retry, copy error | + +### Backends + +| Backend | v0 | Source | +|---|---|---| +| Compose | Full detail | `/v1/projects` or `hack projects --json` (`runtime.services[].containers[]`) | +| Native (beta) | Row and state pill only; Services shows "native: details need R2" | `hack ps --json --path ` (`data.status`) | + +Project views read legacy inputs; a canonical native project comes back with +`input_diagnostic`. The app recognises that code and asks `hack ps` instead +rather than showing "Needs attention". Native `data.status` maps as: +`not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded +from `items[].state`, `runtime_degraded` → Degraded, `runtime_not_running` and +`runtime_busy` and `owner_unconfirmed` → Unknown (with the status as reason), +`provider_pin_changed` → Needs attention. ### Project detail: header per state -Header: name, state pill (+ uptime), backend pill (`compose` / `native`), URL, -buttons, "Always on" switch. Tabs: Services · Logs · Env · Activity. +Header: name, state pill (+ uptime when known), backend pill, URL, buttons, +"Always on" switch. Tabs: Services · Logs · Env · Activity. ![Project states](diagrams/project-state.svg) ([source](diagrams/project-state.mmd)) -| State | Derived from (until request R2 lands) | Pill | Buttons | +| State | Derived from (until R2 lands) | Pill | Buttons | |---|---|---|---| -| Stopped | `status: stopped` | grey "Stopped" | **Start**, Open (disabled) | -| Starting | app's own `up` in flight, or native `phase: pending` | blue spinner "Starting…" + service progress when R6 exists | Stop (cancels) | -| Running | all `defined_services` have a running container | green "Running · 2h 14m" | Open, Restart, Stop | -| Degraded | some running, some defined service not running or unhealthy | amber "Degraded · api down" | Open, Restart, Stop | -| Stopping | app's own `down` in flight | grey spinner "Stopping…" | none | -| Failed start | last `up` envelope has `services.failed`, or a container exited ≠ 0 after start | red "Failed to start: api, worker" | Retry, Logs (filtered to failed services), Stop | -| Needs attention | always-on gave up (R12), `status: missing`, or `input_diagnostic` present | red outline "Needs attention" + one line why | the one fix (Retry / Open folder / Run doctor) | -| Unknown | runtime unreadable | grey "?" "Status unknown" + error | Retry | - -Actions run one at a time per project; buttons disable while one runs. A finished -action re-reads the project at once instead of waiting for the next poll. +| Unknown | runtime unreadable, read failed, or an action timed out or was interrupted | grey "Status unknown" + reason | Start (disabled while engine down) | +| Stopped | no running container and no failed one | grey "Stopped" | Start | +| Starting | this app's `up` in flight, or native `pending` | blue "Starting…" | none (no cancel in v0) | +| Running | every long-running service has a running container; one-shot services that exited 0 count as completed | green "Running · 2 hours" (from Docker status text) | Open, Restart, Stop | +| Degraded | some running; a long-running service down, exited ≠ 0, or `(unhealthy)` | amber "Degraded · worker down" | Open, Restart, Stop | +| Stopping | this app's `down` in flight | grey "Stopping…" | none | +| Failed start | last `up` reported `services.failed`, or nothing runs and a service exited ≠ 0 | red "Failed to start: api, worker" | Start (retry), Stop, logs filtered to those services | +| Needs attention | always-on gave up (R12b), `status: missing`, or an `input_diagnostic` that is not the native one | red "Needs attention" + one line why | the one fix | + +Rules: +- One action per project at a time; all action buttons disable while it runs. + Cancel is not offered in v0 (`hack up` has no cancel contract). +- A finished action re-reads at once. Success is what the next read observes, + not the command's exit: `down` exiting 0 shows Stopping until a read shows it stopped. +- A failed Stop or Restart keeps the last observed state and adds "Last action + failed: : " under the header. +- An action that times out or loses its process → Unknown until the next read. +- Exit codes and uptime come from Docker's status text (`Exited (1) …`, `Up 2 hours`); + structured fields are request R2. A one-shot service is one with `restart: "no"` + semantics as Hack's startup state reports it; until R2 exposes that, an exit 0 + is treated as completed, never as down. ### Services tab | State | Shown | |---|---| -| Running / degraded | Table: service, state (dot + text), CPU %, memory, route (host link). Down services sort first. Short log tail (last 20 lines, all services) below. | -| Stopped | Table of `defined_services` with "—" for CPU and memory; no tail. | -| Usage unavailable | CPU and memory show "—" with a tooltip ("usage needs R3"). | -| Failed start | Failed rows red with exit code; tail is filtered to those services. | +| Running / degraded | Table: service, state (dot + Docker status text), CPU %, memory, route. Down services first. Short log tail (last 20 lines) below. | +| Stopped | `defined_services` with "not created"; no tail. | +| Usage unavailable | CPU and memory "—" with tooltip (needs R3). | +| Failed start | Failed rows red with exit code; tail filtered to them. | ### Logs tab -Source: `hack logs --json --path [--services a,b] --tail 500` (ndjson, follow by default). +Source: `hack logs --json --path --tail 500` (ndjson, follows by default). | State / control | Behaviour | |---|---| | Follow (default) | Newest at bottom; auto-scroll while at the bottom. | -| Pause | Stops auto-scroll; keeps reading into a 10,000-line ring buffer; shows "N new lines" chip. Scrolling up pauses. | -| Search | Filters the buffer in place (plain text, case-insensitive); shows match count. | -| Service filter | Chips per service; changing it restarts the stream with `--services`. | -| Very long lines | Cut at 4 KB on screen with "… (+N KB)"; click to expand; `entry.truncated` shown as "truncated by hack". | -| Secrets warning | Fixed one-line note: "Logs are shown as your services print them. Hack does not hide secrets in logs." No copy-all button in v0. | -| Stream ended / error | `end` or `error` event: banner with reason and Reconnect. Engine down: "Engine not running". | +| Pause | Stops auto-scroll; keeps reading into a ring buffer bounded at 10,000 lines **and** 8 MB; "N new lines" chip. Scrolling up pauses. | +| Search | Filters the buffer in place (plain text, case-insensitive), with match count. | +| Service filter | One service at a time: restarts the stream as `hack logs `. `--services` is Loki-only and native rejects it (and `--compose`), so the app never passes either. Multi-select needs R4. | +| Very long lines | Cut at 4 KB on screen with "… (+N KB)", click to expand; `entry.truncated` shown as "truncated by hack". | +| Secrets warning | Fixed note: "Logs are shown as your services print them. Hack does not hide secrets in logs." No copy-all; buffer never written to disk; "Open in terminal" offered. | +| Finished | `end` event on a `--no-follow` read: "End of log" (normal). | +| Stream error | `error` event or exit ≠ 0: banner with reason and Reconnect. Engine down: "Engine not running". | | Stopped project | Last 200 lines (`--no-follow`), marked "project stopped". | ### Env tab (names only, never values) @@ -97,85 +125,80 @@ Source: `hack env list --json --path `. | State | Shown | |---|---| -| Normal | Table: name, layer (`layer` / `resolved_from`), scope, kind (secret or plain from `storage.kind`), set (`has_value` ✓/✗), required. | -| Missing required | Red rows from `missing_required`, top of table. | -| Locked | Key missing or unreadable: lock icon, "Env key not available: names are shown, values cannot be resolved", one action (copy `hack doctor` command). Needs R10 to detect reliably. | +| Normal | Table: name, layer (`layer` / `resolved_from`), scope, kind (`storage.kind`), set (`has_value` ✓/✗), required. | +| Missing required | Red rows from `missing_required`, first. | +| Locked | Env key missing or unreadable: lock icon, "Names are shown; values cannot be resolved", Copy `hack doctor`. Needs R10. | | No env config | "This project has no env layers" + docs link. | -Editing is out of scope until "later". Never a "reveal" button. +Editing is "later". Never a reveal button. ### Activity tab -Needs R5. Until then the tab shows only actions this app ran in this session (kept -in memory, gone on quit) with the note "Full history needs a newer hack". +Needs R5. Until then it shows only actions this app ran in this session (memory +only) with "Full history needs a newer hack". | Row kind | Fields | |---|---| -| Start / stop / restart | time, who (`cli` / `app` / `autostart` / `agent`), duration, result | +| Start / stop / restart | time, origin (`cli` / `app` / `autostart` / `agent`), duration, result | | Failure | time, failed services, error code, link to logs at that time | | Always-on | paused after manual stop, resumed, gave up after N tries | ### Status bar -Left: "3 running · 5 stopped". Middle: engine state (Docker or native, ok/down), -hackd (on / off / version mismatch). Right: CLI version, "Update available" chip. +Left: "3 running · 5 stopped". Middle: engine (ok / down / unknown), hackd (on / +off / version mismatch). Right: CLI version, "Update available" chip. With no +snapshot, only counts and nothing else. ## 2. First-run onboarding -Shown on first launch and from the Doctor row ("Run setup again"). One check per -step, in order; each step is a card with status, why it matters, exact command. +Shown on first launch and from the Doctor row ("Run setup again"). One card per +check, in order. Every check is read-only; every card offers **Check again**. ![Onboarding](diagrams/onboarding.svg) ([source](diagrams/onboarding.mmd)) -| Step | Check (read-only) | Fix command | Admin | Skipped shows | +| Step | Read-only check | Fix | Admin | Skipped / unknown shows | |---|---|---|---|---| -| CLI | `hack version --json` (R1) | `brew install hack-dance/tap/hack` | no (app only copies it) | Cannot skip: nothing works without it | -| Engine | `runtime_ok` from `/v1/projects` or `hack projects --json` | open Docker Desktop / docs; native: `hack native setup` style command (HACK-2193) | no | Banner on every project: "Engine not running" | +| CLI | `hack --version` (text; R1 for JSON) | `brew install hack-dance/tap/hack` (copy) | no | Cannot skip | +| Engine | `runtime_ok` from `/v1/projects` or `hack projects --json` | open Docker Desktop / engine docs; native setup command (HACK-2193) | no | "Engine not running" on every project | | hackd | `hack daemon status --json` | `hack daemon start` | no | Status bar "hackd off"; slower refresh | -| Routing and DNS | `hack global status --json` `summary.host_dns_ok`, `caddy_ok` | `hack global install` | **yes** (resolver files, dnsmasq or `resolvectl`) | URLs shown greyed with "DNS not set up"; Open uses `localhost` ports if known | -| Certificate trust | doctor check `caddy local ca` | `hack global trust` | **yes** (System keychain / system trust store) | Lock icon on URLs: "browser will warn" | -| Login service | `daemon status` `service.installed` | `hack daemon install` | no | Always-on switch disabled: "needs the login service" | - -Every step: **Run** (non-admin, the app runs the command and streams its output), -**Open in Terminal** (admin), **Copy command**, **Skip**, **Check again**. Skips -are not remembered as decisions: the next check simply finds the step still open -and Doctor lists it. There is no hidden "don't ask again" state. +| Routing and DNS | macOS: doctor `dnsmasq:53` and `coredns forwarding`; native: `native host dns`. Linux: **unknown** until R13 (only `hack global status`, which can start Docker, reports it) | `hack global install` | **yes** | URLs greyed "DNS not set up"; "unknown" when no check reports | +| Certificate trust | **Unknown** today for Compose: doctor `caddy local ca` checks the exported CA, not system or browser trust (R16). Native: `native root trust` | `hack global trust` | **yes** | Lock icon on URLs: "browser may warn" | +| Login service | `daemon status` `service.installed` | `hack daemon install` | no | Always-on switch disabled | -### Admin steps: system prompt vs terminal hand-off +Skips are not stored: the next check finds the step still open and Doctor lists +it. Non-admin **Run** executes the command and shows its output, then re-checks. +Admin steps offer **Copy command** and **Open Terminal**; neither is assumed to +have succeeded until **Check again** (also run when the window regains focus). ![Admin hand-off](diagrams/admin-handoff.svg) ([source](diagrams/admin-handoff.mmd)) -| | System prompt (macOS `do shell script … with administrator privileges`, Linux `pkexec`) | Terminal hand-off | +Terminal hand-off (recommended for both platforms; comparison in [Appendix A](#appendix-a-system-prompt-vs-terminal)): + +| Platform | Open Terminal adapter | Otherwise | |---|---|---| -| What runs as root | A command the app built | The CLI's own command; the CLI calls `sudo` only where it needs it | -| Mixed user/root steps | Breaks: running `hack global trust` as root writes root-owned files into the user's Hack home | Works: the CLI already splits user and root steps | -| User can read what runs | Only what the app shows | Yes, typed in the terminal before Enter | -| Linux availability | `pkexec` needs a running polkit agent; varies by desktop | Any terminal (`xdg-terminal-exec`, `$TERMINAL`, fallbacks) | -| Polish | Native dialog, Touch ID | One extra window | +| macOS | Terminal.app: a new window runs a one-time script from the app's private temp dir that prints the quoted command and waits for Enter before running it | Copy command | +| Linux | `xdg-terminal-exec` when present, same script | Copy command | -Recommendation: **terminal hand-off on both platforms for v0.** The app writes -nothing executable: it opens the terminal with the command typed and waiting for -Enter, then re-runs the check when the terminal process exits (or on "Check -again"). macOS system prompt can come later, only for single root-only commands -the CLI exposes as an argv plan (R11), never for whole `hack` commands. +The script holds only the CLI's own command with each argument shell-quoted; it +is removed after use. The app never types into an existing terminal or pane. ## 3. Doctor -Source: `hack doctor --json` (today: `checks[{id,status,detail}]`), on open of the -Doctor row and every 10 minutes while the window is visible. +Source: `hack doctor --json --path ` per registered project, plus one run +outside any project for machine checks (until R7c), on open and every 10 +minutes while visible. Today it gives `checks[{id,status,detail}]`. | Card part | Content | |---|---| | Group | Area: Engine, Routing and DNS, Certificates, Daemon, Env and secrets, Project files, Native | | Severity | `error` red, `warn` amber; `ok` collapsed into "12 checks passed" | -| Affected | "Machine" or the project names it applies to | -| One action | Run safe fix (non-admin, `hack doctor --fix --only `, R7) · Open in Terminal (admin) · Copy command · Open doc | +| Affected | "Machine" or the projects it applies to | +| One action | Run safe fix (R7b) · Open Terminal (admin) · Copy command · Open doc | -Machine-wide findings appear once under their area, never repeated on each -project. A project's own findings show as a "Needs attention" line in its header -that links to the Doctor card. Until R7 adds `area`, `scope` and `fix`, the app maps -known check `id`s to areas with a small table in `core` and shows unknown ids under -"Other" with only Copy and Open doc. +Machine findings are de-duplicated by check `id` and shown once; project +findings link from that project's header. Until R7a adds `area`, `scope` and +`fix`, `core` maps known `id`s to areas and shows unknown ones under "Other" +with only Copy and Open doc. No Run button appears without a `fix.kind: safe`. ## 4. Upgrades @@ -183,119 +206,153 @@ known check `id`s to areas with a small table in `core` and shows unknown ids un | State | Source | Shown | Action | |---|---|---|---| -| Up to date | `hack update --check --json` `updateAvailable: false` (daily) | nothing | none | -| Available | `updateAvailable: true`, `latest` | Status bar chip "Hack vX available" | Review | -| Checking | `hack upgrade check --json` (#790, read-only) | Spinner, then plan | none | -| Blocked | `ready: false`, `blockers[]` | One card per blocker with its `command` or `next_action` | Run / Copy, Re-check | -| Incomplete | `incomplete[]` | "Could not check: …" cards; treated like Blocked | Re-check | -| Ready | `ready: true`, `plan[]` | Ordered plan list (machine steps, then projects) | Upgrade | -| Snapshotting | `hack upgrade snapshot --json` | Progress; manifest path kept for rollback | none | -| Applying | Homebrew install: Terminal `brew upgrade hack` (formula and cask); then plan steps | Step list with ✓/✗ | none | -| Restarting daemon | `hack daemon restart` (HACK-2216), then `/v1/status.version` == CLI version | "Restarting hackd…" | none | -| Failed | any step exit ≠ 0, or versions still differ | Which step, its error, and "Your files were saved at snapshot …" | Roll back, Copy error | -| Rolling back | `hack upgrade rollback --rollback-manifest --json` | Restored / could not restore list (`keptAs`, reasons) | none | - -The app never upgrades itself separately: the cask and formula move together, and -the app restarts itself only after the user confirms ("Relaunch to finish"). +| Update available | R14 (read-only, any install kind). `hack update --check` rejects Homebrew installs and candidate launchers, so it is not used | Chip "Hack vX available" | Review | +| Checking | `hack upgrade check --json` (#790) | Spinner, then plan | none | +| Blocked | `ready: false`. `blockers[]` / `incomplete[]` are `{scope,id,summary}`; the command or action comes from the matching finding (`machine[]` or `projects[].findings[]` by scope + id) | One card per blocker | Copy / Open Terminal; Run only with R7a's `kind: safe`; Re-check | +| Ready | `ready: true`, `plan[]` (`command` / `args` / `action`, `blocker`) | Ordered plan | Upgrade | +| Snapshotting | `hack upgrade snapshot --json` | Progress | none | +| Snapshot incomplete | `complete: false` or `gaps[]` | Gaps listed; Upgrade stays disabled | Re-check | +| Applying | Homebrew: `brew upgrade hack` **and** `brew upgrade --cask hack-desktop`, in a terminal, then plan steps | Steps with ✓/✗; partial success named ("CLI upgraded, app not") | none | +| Restarting daemon | `hack daemon restart` (exists), then `/v1/status.version` == CLI version. Doing this automatically after upgrade is HACK-2216 (proposed) | "Restarting hackd…" | none | +| Failed | a step exit ≠ 0, or versions still differ | Which step and its error; "Files were saved" only when a complete snapshot exists | Roll back (only with a complete snapshot), Copy error | +| Unknown | terminal step interrupted or not confirmed | "Could not confirm the upgrade" | Re-check | +| Rolling back | `hack upgrade rollback --rollback-manifest --json` | Restored / not restored (`kept_as`, reasons) | none | +| Restored | files restored | "Files restored. Versions, containers and services were not changed." | Re-check | + +The app does not update itself apart from Homebrew, and relaunches only after +the user confirms. ## 5. Daemon Source: `hack daemon status --json` + `GET /v1/status` (version). +`start`, `restart`, `install` print no JSON today: the app uses the exit code and +then re-reads status (R15 would add `--json`). | State | Detected by | Shown | One action | |---|---|---|---| -| Not installed, not running | `running: false`, `service.installed: false` | Status bar "hackd off" | Start (`hack daemon start`); setup offers `hack daemon install` | -| Installed, stopped | `service.installed: true`, `running: false` | "hackd stopped (login service installed)" | Start (`hack daemon start`, goes through the service manager) | -| Running, other version | `/v1/status.version` ≠ `hack version` | "hackd vX, CLI vY" amber | Restart (`hack daemon restart`, HACK-2216) | -| Stale | `stale: true` (`stale_reason`), or unit points at a missing or other binary (R8) | "hackd unit is stale" + `next_step` | Repair (`hack daemon install`), else Copy `next_step` | -| Incompatible / starting | `status` | as given, with `issue` | Wait / Restart | +| Not installed, not running | `running: false`, `service.installed: false` | "hackd off" | Start (`hack daemon start`); setup offers `hack daemon install` | +| Installed, stopped | `service.installed: true`, `running: false` | "hackd stopped (login service installed)" | Start | +| Running, other version | `/v1/status.version` ≠ CLI version | "hackd vX, CLI vY" amber | Restart (`hack daemon restart`) | +| Stale process files | `stale: true` (`stale_reason`: `pid_not_running` / `socket_only`) | "hackd stopped unexpectedly" | Start (cleans the stale files) | +| Stale unit | unit points at a missing or other binary (R8) | "Login service is out of date" + `service.next_step` | Repair (`hack daemon install`) | +| Starting / incompatible | `status` | as given, with `issue` | Wait / Restart | | Running | ok | green dot only | none | The app never kills a daemon process or edits a unit itself. -## 6. Always-on (HACK-2213) +## 6. Always-on (HACK-2213, proposed contract) ![Always-on](diagrams/always-on.svg) ([source](diagrams/always-on.mmd)) | State | Shown on the project | Action | |---|---|---| | Off | Switch off | Switch on → `hack autostart add --project ` | -| On | Switch on, pin in sidebar | Switch off → `hack autostart remove --project ` | -| Paused until you start it | After a manual Stop: switch on, label "Paused until you start it" | Start (resumes) | -| Needs attention | After N failed starts: red label "Always-on stopped trying after 3 failures" + failed services | Retry (`hack autostart resume --project`), Logs, Turn off | -| Globally paused | Banner "Always-on paused for all projects" + menu item | Resume all (`hack autostart resume`) | -| At login | While the login run is in progress: sidebar header "Starting 2 of 4…", each pending row in Starting | none; failures become Needs attention | -| No login service | Switch disabled, tooltip "needs `hack daemon install`" | Install login service | +| On | Switch on, "always on" mark in sidebar | Switch off → `hack autostart remove --project ` | +| Paused until you start it | after a manual Stop: label "Paused until you start it" | Start (resumes); Switch off | +| Needs attention | after `max_failures` failed starts: "Always-on stopped trying after 3 failures" + failed services | Retry (`hack autostart resume --project`), Logs, Switch off | +| Globally paused | Banner "Always-on paused for all projects" | Resume all (`hack autostart resume`) | +| At login | while `login_run.in_progress`: "Starting 2 of 4…", pending rows Starting | none; failures become Needs attention | +| No login service | Switch disabled: "needs `hack daemon install`" | Install login service | +| Command failed | any `autostart` command exit ≠ 0 | Switch returns to its previous position; error under the header | Retry | -Fields and commands the app needs from the CLI side (design contract for HACK-2213): +Fields and commands needed from the CLI side: | Where | Field / command | |---|---| -| per project in `hack status --json` / `/v1/projects` | `autostart.enabled: bool`, `autostart.state: "off" \| "on" \| "paused" \| "starting" \| "needs_attention"`, `autostart.paused_reason: "manual_stop" \| null`, `autostart.failures.count`, `autostart.failures.last_at`, `autostart.failures.failed_services[]`, `autostart.failures.last_error_code`, `autostart.max_failures` | -| global in the same payload | `autostart_global.paused: bool`, `autostart_global.login_run: { in_progress, started_at, pending[], started[], failed[] }`, `autostart_global.service_ready: bool` | -| commands (all `--json --no-interactive`) | `hack autostart add\|remove --project `, `hack autostart list --json`, `hack autostart pause [--project ]`, `hack autostart resume [--project ]` (resume also clears the failure count) | -| behaviour | `hack down` on an always-on project sets `paused/manual_stop`; `hack up` clears it. The app relies on that, not on its own bookkeeping. | +| per project in `hack projects --json` / `/v1/projects` | `autostart.enabled`, `autostart.state: "off" \| "on" \| "paused" \| "starting" \| "needs_attention"`, `autostart.paused_reason: "manual_stop" \| null`, `autostart.failures.{count,last_at,failed_services[],last_error_code}`, `autostart.max_failures` | +| global, same payload | `autostart_global.paused`, `autostart_global.login_run.{in_progress,started_at,pending[],started[],failed[]}`, `autostart_global.service_ready` | +| commands (`--json --no-interactive`) | `hack autostart add\|remove --project `, `hack autostart list --json`, `hack autostart pause [--project ]`, `hack autostart resume [--project ]` (resume clears the failure count) | +| behaviour | `hack down` on an always-on project sets `paused/manual_stop`; `hack up` clears it. The app relies on that, not its own bookkeeping. | ## 7. Data contract -Polling stops when the window is hidden (menu bar popover later polls only while -open). Every CLI call has a timeout and is killed with its process group on expiry. +Polling stops while the window is hidden. Every CLI call has a timeout and is +killed with its process group on expiry; a timeout makes the screen Unknown. | Screen | Reads | How often | Timeout | |---|---|---|---| -| Sidebar, header | `GET /v1/projects?summary=1` if `/v1/status.version` matches, else `hack projects --json --summary` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | -| Detail (selected) | `GET /v1/projects?filter=` (else `hack projects --json`), plus `hack ps --json --path` for native | 2 s while selected | 3 s / 15 s | -| Services CPU/memory | `hack usage --json --details` (until R3) | 5 s while Services is visible | 10 s | -| Logs | `hack logs --json --path --tail 500` (one long-running child) | stream | none; killed on tab change | +| Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI, else `hack projects --json --no-daemon`; native rows add `hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | +| Services CPU/memory | `hack usage --json --details` (until R3) | 5 s while visible | 10 s | +| Logs | `hack logs --json --path [] --tail 500` (one long-running child) | stream | killed on tab change | | Env | `hack env list --json --path ` | on tab open, after env actions | 10 s | -| Activity | `hack activity --json --project --limit 100` (R5) | on tab open, 10 s while open | 10 s | -| Doctor, onboarding | `hack doctor --json`, `hack global status --json`, `hack daemon status --json` | on open; every 10 min | 60 s | -| Daemon | `GET /v1/status` | every 10 s | 1 s | -| Upgrades | `hack update --check --json` daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | -| Actions | `hack up -d`, `down`, `restart`, `open --json`, `daemon start\|restart\|install`, `autostart …` | on click | 10 min for `up`, 2 min others | +| Activity | `hack activity --json --project --limit 100` (R5b) | on open, 10 s while open | 10 s | +| Doctor, onboarding | `hack doctor --json` (per project and once outside), `hack daemon status --json` | on open; every 10 min | 60 s | +| Daemon | `GET /v1/status` | 10 s | 1 s | +| Upgrades | R14 daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | +| Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | 10 min for `up`, 2 min others | ### Requests to the CLI and daemon (one PR each, for other sessions) -1. **R1 `hack version --json`**: `{version, channel, install_kind, binary_path}`, so the app can check the supported range and say how to upgrade. -2. **R2 project `state` field** in `/v1/projects` and `hack projects --json`: `running | degraded | starting | stopping | stopped | failed | unknown`, plus `started_at` and `failed_services[]`. Moves state derivation out of the app. -3. **R3 per-service usage**: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes` per service. -4. **R4 log stream guarantees**: document `hack logs --json` ndjson as a stable contract and add `--max-line-bytes ` so the CLI sets `entry.truncated`. -5. **R5 activity history**: append-only record written by `up`, `down`, `restart` and autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), read with `hack activity --json [--project] [--limit]`. -6. **R6 action progress**: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` lines before the final envelope. -7. **R7 structured doctor findings**: add `area`, `scope: machine | project`, `projects[]`, `fix: {kind: safe | admin | manual, args[], doc_url}` per check (same shape as `UpgradeFinding` in #790), and `hack doctor --fix --only --yes --json` for one safe fix without prompts. -8. **R8 daemon status version and unit staleness**: `version` and `cli_version` in `hack daemon status --json`, plus `service.unit_stale` and its reason. -9. **R9 push updates (optional)**: `GET /v1/projects?wait=` long-poll, so the app can stop 2-second polling. -10. **R10 env key state**: top-level `key_state: ok | missing | unreadable` in `hack env list --json` (no values). -11. **R11 admin argv plan**: `hack global install|trust --plan --json` listing the exact root-only argv lines, so the app can show them and later hand single lines to the macOS system prompt. -12. **R12 always-on fields and commands** as listed in [section 6](#6-always-on-hack-2213). +1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. +2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. +3. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. +4. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. +5. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`). +6. **R5b** Activity reader: `hack activity --json [--project] [--limit]`. +7. **R6** Action progress: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` before the final envelope. +8. **R7a** Doctor metadata: per check `area`, `scope: machine | project`, `fix: {kind: safe | admin | manual, args[], doc_url}` (same shape as #790's findings plus `kind`). +9. **R7b** One fix without prompts: `hack doctor --fix --only --yes --json`, only for `kind: safe`. +10. **R7c** Machine-only doctor: `hack doctor --machine --json` (no project), so the app does not need to de-duplicate. +11. **R8** Daemon unit staleness: `service.unit_stale` and reason in `hack daemon status --json`, and `version` there. +12. **R9** (optional) Push updates: `GET /v1/projects?wait=` long-poll. +13. **R10** Env key state: `key_state: ok | missing | unreadable` in `hack env list --json` (no values). +14. **R11** Admin argv plan: `hack global install|trust --plan --json` listing the root-only argv lines. +15. **R12a** Always-on persistence and `hack autostart add|remove|list|pause|resume` ([section 6](#6-always-on-hack-2213-proposed-contract)). +16. **R12b** Always-on fields in project payloads and `autostart_global`. +17. **R12c** Login run: hackd starts always-on projects at service start, retries up to `max_failures`, and honours pause. +18. **R13** Observe-only `hack global status --json --observe` that never starts Docker; reports `host_dns_ok` on both platforms. +19. **R14** Read-only update check for any install kind: `hack update --check --json --observe` → `{current, latest, available, install_kind, upgrade_command}`. +20. **R15** `--json` for `hack daemon start|stop|restart|install`. +21. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. +22. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. ## 8. Scope | Version | Contains | |---|---| -| v0 | Window, sidebar, detail header, Services table, log tail, status bar, light/dark; Start, Stop, Restart, Open, follow logs; `--fixtures ` mode. | +| v0 | Window, sidebar, detail header, Services table, log tail, status bar, light/dark; Start, Stop, Restart, Open, follow logs; `--fixtures `. Compose detail; native row state only. | | v0.1 | Logs tab, Doctor view, onboarding, Env tab (names), always-on and daemon states, upgrade flow; Homebrew cask; Linux tarball. | | Later | Menu bar popover (open/start/stop, "Pause always-on"); activity charts; env editing through `hack env set`; branch instances; resource graphs. | -| Never | Edit Compose files or Hack config; show, copy or export secret values; install, start or manage Docker itself; ask for or store a password; an embedded terminal, editor or agent; remote/gateway/tickets; its own database. | +| Never | Edit Compose files or Hack config; show, copy or export Hack's secret values; install, start or manage Docker itself; ask for or store a password; an embedded terminal, editor or agent; remote/gateway/tickets; its own database. | Menu bar: gpui-kit 0.9.1 and GPUI have no tray or status item API (only the app menu, dock menu and in-window menus). The popover needs a separate crate such as -`tray-icon`, not yet checked against GPUI's event loop. That is why it is "later". +`tray-icon`, not yet checked against GPUI's event loop; hence "later". ## 9. Distribution | Item | Plan | |---|---| -| macOS | Cask `hack-desktop` in `hack-dance/homebrew-tap` beside `Formula/hack.rb`, `depends_on formula: "hack"`; `brew install --cask hack-desktop`. The formula offers it in caveats only; the CLI install never pulls the app. | -| Signing | Developer ID Application certificate, hardened runtime, no App Sandbox (the app runs `hack` and reads the hackd socket). Notarize with `notarytool submit --wait`, then `stapler staple`. Certificate, its password and the notary API key live only in the owner's password manager and CI secrets; this design sets none of them up. | -| Release job | A separate, manual workflow later builds the universal `.app`, signs, notarizes, zips, and updates the cask with its sha256. It does not join the CLI release until the lead decides. | -| Auto-update | None. Homebrew owns updates (`brew upgrade`), and the app only shows "update available". gpui-kit ships no updater. | -| Linux | v0.1: tarball with binary, `.desktop` file and icon, plus an `install.sh` into `~/.local`. Later maybe `.deb`. No Flatpak/Snap: their sandboxes block running `hack` and reading the socket. | -| Minimums | macOS 15+. Linux: Wayland or X11 with Vulkan (software Vulkan works), glibc of Ubuntu 24.04 or newer. Windows: not planned. | -| Version match | The app reads `hack version --json` and supports a declared CLI range; outside it, it shows only the "update Hack" card. | +| macOS | Cask `hack-desktop` in `hack-dance/homebrew-tap` beside `Formula/hack.rb`, `depends_on formula: "hack"`. Opt-in: the formula mentions it in caveats; the CLI install never pulls it. Upgrading needs both `brew upgrade hack` and `brew upgrade --cask hack-desktop`; they are not atomic. | +| Signing | Developer ID Application certificate, hardened runtime, no App Sandbox (the app runs `hack` and reads the hackd socket); notarize with `notarytool submit --wait`, then `stapler staple`. Keys live only in the owner's password manager and CI secrets; nothing is set up here. Steps: [Appendix B](#appendix-b-release-steps). | +| Auto-update | None. Homebrew owns updates; the app only shows "update available". gpui-kit ships no updater. | +| Linux | v0.1: tarball (binary, `.desktop`, icon, `install.sh` into `~/.local`). No Flatpak/Snap: their sandboxes block running `hack` and reading the socket. | +| Minimums | macOS 15+. Linux: Wayland or X11 with Vulkan (software Vulkan works), Ubuntu 24.04-era glibc or newer. Windows: not planned. | +| Version match | The app declares a supported CLI range; outside it, only the "update Hack" card shows. | ## Open questions for the lead -1. Cask name `hack-desktop` and install opt-in only (recommended), or offered by `hack setup`? -2. Who runs always-on at login: hackd at service start (recommended) or a separate unit? -3. Is terminal hand-off acceptable for v0 admin steps on macOS (recommended), or is the system prompt a v0 requirement? +1. Cask name `hack-desktop`, opt-in only (recommended), or offered by `hack setup`? +2. Who runs always-on at login: hackd at service start (recommended, R12c) or a separate unit? +3. Terminal hand-off for v0 admin steps on macOS (recommended), or is the system prompt required? +4. Logs in the app with a warning (as briefed), or terminal-only logs? + +## Appendix A: system prompt vs terminal + +| | System prompt (macOS `do shell script … with administrator privileges`, Linux `pkexec`) | Terminal hand-off | +|---|---|---| +| What runs as root | A command the app built | The CLI's own command; the CLI calls `sudo` only where needed | +| Mixed user/root steps | Breaks: `hack global trust` as root writes root-owned files into the user's Hack home | Works: the CLI already splits user and root steps | +| User can read what runs | Only what the app shows | Yes, in the terminal before Enter | +| Linux availability | `pkexec` needs a running polkit agent; varies by desktop | Any terminal with an adapter, else Copy | +| Polish | Native dialog, Touch ID | One extra window | + +A macOS system prompt could come later for single root-only lines from R11, never for whole `hack` commands. + +## Appendix B: release steps + +1. Build a universal `.app` (`aarch64` + `x86_64`) with `Info.plist` (`dance.hack.desktop`, minimum 15.0). +2. `codesign --options runtime --timestamp` with the Developer ID Application identity from CI secrets. +3. Zip, `xcrun notarytool submit --wait` with the notary API key from CI secrets, `xcrun stapler staple`, `stapler validate`. +4. Upload the zip to the release; update the cask's `version` and `sha256` in the tap (as the formula job does today). +5. A separate, manual workflow does this until the lead folds it into the release. diff --git a/apps/desktop/diagrams/admin-handoff.mmd b/apps/desktop/diagrams/admin-handoff.mmd index 50d26d60c..29fd09927 100644 --- a/apps/desktop/diagrams/admin-handoff.mmd +++ b/apps/desktop/diagrams/admin-handoff.mmd @@ -5,14 +5,18 @@ sequenceDiagram autonumber actor U as User participant A as App - participant T as Terminal + participant T as New terminal window participant C as hack CLI A->>U: Explain step, show exact command - U->>A: Open in Terminal - A->>T: New window, command typed, waits for Enter - U->>T: Enter - T->>C: hack global trust - C->>U: sudo asks for password (in Terminal) - C-->>T: exit code - A->>C: re-run the check (hack doctor --json) - A->>U: Step done, failed, or skipped + alt Open Terminal (supported adapter) + U->>A: Open Terminal + A->>T: New window shows command, waits for Enter + U->>T: Enter + T->>C: hack global trust + C->>U: sudo asks for password (in the terminal) + else Copy command + U->>A: Copy, run it anywhere + end + U->>A: Check again + A->>C: read-only check + A->>U: Done, still needed, or unknown diff --git a/apps/desktop/diagrams/admin-handoff.svg b/apps/desktop/diagrams/admin-handoff.svg index 2260159a4..9e9675072 100644 --- a/apps/desktop/diagrams/admin-handoff.svg +++ b/apps/desktop/diagrams/admin-handoff.svg @@ -1,4 +1,4 @@ -Admin step · terminal hand-off123456789Explain step, show exact commandOpen in TerminalNew window, command typed, waits for EnterEnterhack global trustsudo asks for password (in Terminal)exit codere-run the check (hack doctor --json)Step done, failed, or skippedUserAppTerminalhack CLIUserAppTerminalhack CLI \ No newline at end of file +]]>Admin step · terminal hand-off[Copy command]alt[Open Terminal (supported adapter)]12345678910Explain step, show exact commandOpen TerminalNew window shows command, waits for EnterEnterhack global trustsudo asks for password (in the terminal)Copy, run it anywhereCheck againread-only checkDone, still needed, or unknownUserAppNew terminal windowhack CLIUserAppNew terminal windowhack CLI \ No newline at end of file diff --git a/apps/desktop/diagrams/always-on.mmd b/apps/desktop/diagrams/always-on.mmd index 3cfc8008f..36479a6cc 100644 --- a/apps/desktop/diagrams/always-on.mmd +++ b/apps/desktop/diagrams/always-on.mmd @@ -4,13 +4,18 @@ title: Always-on · one project stateDiagram-v2 direction TB [*] --> Off - Off --> On : switch on - On --> Off : switch off + Off --> On : switch on ok + On --> Off : switch off ok On --> Paused : manual Stop Paused --> On : manual Start - On --> NeedsAttention : 3 failed starts + Paused --> Off : switch off + On --> NeedsAttention : max failures NeedsAttention --> On : Retry ok NeedsAttention --> Off : switch off + note right of Off + INV a failed autostart command + leaves the switch where it was + end note note right of On INV global pause holds every project without changing it diff --git a/apps/desktop/diagrams/always-on.svg b/apps/desktop/diagrams/always-on.svg index 2fb84e7d2..e11c77831 100644 --- a/apps/desktop/diagrams/always-on.svg +++ b/apps/desktop/diagrams/always-on.svg @@ -1,4 +1,4 @@ -Always-on · one projectswitch onswitch offmanual Stopmanual Start3 failed startsRetry okswitch offOffPausedNeedsAttentionOnINV global pause holds everyproject without changing it \ No newline at end of file +]]>Always-on · one projectswitch on okswitch off okmanual Stopmanual Startswitch offmax failuresRetry okswitch offPausedNeedsAttentionOffOnINV a failed autostart commandleaves the switch where it wasINV global pause holds everyproject without changing it \ No newline at end of file diff --git a/apps/desktop/diagrams/onboarding.mmd b/apps/desktop/diagrams/onboarding.mmd index 0c3bda950..cbb65537a 100644 --- a/apps/desktop/diagrams/onboarding.mmd +++ b/apps/desktop/diagrams/onboarding.mmd @@ -1,26 +1,18 @@ --- -title: First run · checks in order +title: First run · one step (repeated per check) --- flowchart TD - S([App opens]) --> C1{"hack CLI found,
version supported?"} - C1 -->|no| X1["Install CLI card
(brew command)"] --> C1 - C1 -->|yes| C2{"Engine running?"} - C2 -->|no| X2["Engine card:
open Docker / native setup"] - C2 -->|yes| C3{"hackd running?"} - X2 -. skip .-> C3 - C3 -->|no| X3["Start hackd
(no admin)"] - C3 -->|yes| C4{"Routing + DNS ok?"} - X3 -. skip .-> C4 - C4 -->|no| X4["DNS step
needs admin"] - C4 -->|yes| C5{"Certificate trusted?"} - X4 -. skip .-> C5 - C5 -->|no| X5["Trust step
needs admin"] - C5 -->|yes| C6{"Login service?"} - X5 -. skip .-> C6 - C6 -->|no| X6["Install login service
(no admin, optional)"] - C6 -->|yes| Done([Project list]) - X6 -. skip .-> Done + S([Step card]) --> C{"Observe
(read-only)"} + C -->|ok| Next([Next step]) + C -->|unknown| U["Unknown: show why,
Check again"] + C -->|needs action| A{"Admin rights?"} + A -->|no| R["Run (app runs hack)"] + A -->|yes| T["Copy command /
Open Terminal"] + R --> C + T -->|Check again| C + U -->|Check again| C + C -. Skip .-> Next classDef wait stroke-dasharray:2 2 classDef ok stroke-width:1.6px - class X4,X5 wait - class Done ok + class T wait + class Next ok diff --git a/apps/desktop/diagrams/onboarding.svg b/apps/desktop/diagrams/onboarding.svg index 1262eede0..74be978f9 100644 --- a/apps/desktop/diagrams/onboarding.svg +++ b/apps/desktop/diagrams/onboarding.svg @@ -1,4 +1,4 @@ -First run · checks in ordernoyesnoyesskipnoyesskipnoyesskipnoyesskipnoyesskipApp openshack CLI found,version supported?Install CLI card(brew command)Engine running?Engine card:open Docker / native setuphackd running?Start hackd(no admin)Routing + DNS ok?DNS stepneeds adminCertificate trusted?Trust stepneeds adminLogin service?Install login service(no admin, optional)Project list \ No newline at end of file +]]>First run · one step (repeated per check)okunknownneeds actionnoyesCheck againCheck againSkipStep cardObserve(read-only)Next stepUnknown: show why,Check againAdmin rights?Run (app runs hack)Copy command /Open Terminal \ No newline at end of file diff --git a/apps/desktop/diagrams/project-state.mmd b/apps/desktop/diagrams/project-state.mmd index a6fba4839..45f1ac78c 100644 --- a/apps/desktop/diagrams/project-state.mmd +++ b/apps/desktop/diagrams/project-state.mmd @@ -3,26 +3,26 @@ title: Project detail · display states --- stateDiagram-v2 direction TB - [*] --> Stopped - Stopped --> Starting : Start - Starting --> Running : all services up - Starting --> FailedStart : a service exited - Running --> Degraded : a service down - Degraded --> Running : service back - Running --> Stopping : Stop - Degraded --> Stopping : Stop - Stopping --> Stopped : down done - FailedStart --> Starting : Retry - FailedStart --> Stopped : Stop - FailedStart --> NeedsAttention : always-on 3x - NeedsAttention --> Starting : Retry - note right of NeedsAttention - INV the app never retries - by itself + [*] --> Unknown + Unknown --> Observed : observation ok + Observed --> Unknown : observation failed + state Observed { + Stopped --> Starting : Start + Starting --> Running : all ready + Starting --> FailedStart : a service failed + Running --> Degraded : a service down + Degraded --> Running : service back + Running --> Stopping : Stop + Degraded --> Stopping : Stop + Stopping --> Stopped : observed stopped + FailedStart --> Starting : Retry + FailedStart --> NeedsAttention : always-on gave up + } + note right of Unknown + INV a failed or timed-out + read never shows Stopped end note classDef live stroke-width:1.6px - classDef wait stroke-dasharray:2 2 classDef fail stroke-width:2px class Running live - class Starting,Stopping wait - class FailedStart,NeedsAttention,Degraded fail + class FailedStart,NeedsAttention,Degraded,Unknown fail diff --git a/apps/desktop/diagrams/project-state.svg b/apps/desktop/diagrams/project-state.svg index 943f290d6..19d2c035b 100644 --- a/apps/desktop/diagrams/project-state.svg +++ b/apps/desktop/diagrams/project-state.svg @@ -1,4 +1,4 @@ -Project detail · display statesStartall services upa service exiteda service downservice backStopStopdown doneRetryStopalways-on 3xRetryStoppedStartingRunningFailedStartDegradedStoppingNeedsAttentionINV the app never retriesby itself \ No newline at end of file +]]>Project detail · display statesObservedobservation okobservation failedStartall readya service faileda service downservice backStopStopobserved stoppedRetryalways-on gave upStoppedStartingRunningFailedStartDegradedStoppingNeedsAttentionUnknownINV a failed or timed-outread never shows Stopped \ No newline at end of file diff --git a/apps/desktop/diagrams/upgrade.mmd b/apps/desktop/diagrams/upgrade.mmd index 7c108ee23..630e0355b 100644 --- a/apps/desktop/diagrams/upgrade.mmd +++ b/apps/desktop/diagrams/upgrade.mmd @@ -3,25 +3,27 @@ title: Upgrade · app view of the flow --- stateDiagram-v2 direction TB - [*] --> UpToDate - UpToDate --> Available : update --check - Available --> Checking : Review - Checking --> Blocked : blockers + [*] --> Checking + Checking --> Blocked : blockers or incomplete + Checking --> Ready : ready Blocked --> Checking : Re-check - Checking --> Ready : ready true Ready --> Snapshotting : Upgrade - Snapshotting --> Applying : snapshot ok - Snapshotting --> Failed : snapshot refused - Applying --> RestartingDaemon : apply ok + Snapshotting --> Blocked : gaps or refused + Snapshotting --> Applying : complete + Applying --> Unknown : interrupted Applying --> Failed : step failed - RestartingDaemon --> UpToDate : versions match - RestartingDaemon --> Failed : version mismatch + Applying --> RestartingDaemon : steps done + RestartingDaemon --> Checking : verify Failed --> RollingBack : Roll back - RollingBack --> UpToDate : restored + RollingBack --> Restored : files restored RollingBack --> Failed : partial restore - classDef live stroke-width:1.6px + Restored --> Checking : re-check + Unknown --> Checking : re-check + note right of Restored + INV rollback restores files only, + never versions or containers + end note classDef wait stroke-dasharray:2 2 classDef fail stroke-width:2px class Snapshotting,Applying,RestartingDaemon,RollingBack wait - class Blocked,Failed fail - class UpToDate live + class Blocked,Failed,Unknown fail diff --git a/apps/desktop/diagrams/upgrade.svg b/apps/desktop/diagrams/upgrade.svg index b18755ed4..aee4eda1a 100644 --- a/apps/desktop/diagrams/upgrade.svg +++ b/apps/desktop/diagrams/upgrade.svg @@ -1,4 +1,4 @@ -Upgrade · app view of the flowupdate --checkReviewblockersRe-checkready trueUpgradesnapshot oksnapshot refusedapply okstep failedversions matchversion mismatchRoll backrestoredpartial restoreUpToDateAvailableCheckingBlockedReadySnapshottingApplyingFailedRestartingDaemonRollingBack \ No newline at end of file +]]>Upgrade · app view of the flowblockers or incompletereadyRe-checkUpgradegaps or refusedcompleteinterruptedstep failedsteps doneverifyRoll backfiles restoredpartial restorere-checkre-checkCheckingBlockedReadySnapshottingApplyingUnknownFailedRestartingDaemonRollingBackRestoredINV rollback restores files only,never versions or containers \ No newline at end of file From 2381b5cd282a08469c7ac5520832b45aab38796c Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:08:52 -0500 Subject: [PATCH 3/8] docs(desktop): native commands need HACK_RUNTIME_BACKEND=native Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 3a8b39e38..8da6b2c37 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -53,11 +53,13 @@ detail pane, status bar. Light and dark follow the system | Backend | v0 | Source | |---|---|---| | Compose | Full detail | `/v1/projects` or `hack projects --json` (`runtime.services[].containers[]`) | -| Native (beta) | Row and state pill only; Services shows "native: details need R2" | `hack ps --json --path ` (`data.status`) | +| Native (beta) | Row and state pill only; Services shows "native: details need R2" | `HACK_RUNTIME_BACKEND=native hack ps --json --path ` (`data.status`) | Project views read legacy inputs; a canonical native project comes back with `input_diagnostic`. The app recognises that code and asks `hack ps` instead -rather than showing "Needs attention". Native `data.status` maps as: +rather than showing "Needs attention". Every command for a native project (`ps`, `up`, +`down`, `restart`, `logs`) runs with `HACK_RUNTIME_BACKEND=native`, which is how the CLI +selects that backend today. Native `data.status` maps as: `not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded from `items[].state`, `runtime_degraded` → Degraded, `runtime_not_running` and `runtime_busy` and `owner_unconfirmed` → Unknown (with the status as reason), @@ -271,7 +273,7 @@ killed with its process group on expiry; a timeout makes the screen Unknown. | Screen | Reads | How often | Timeout | |---|---|---|---| -| Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI, else `hack projects --json --no-daemon`; native rows add `hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | +| Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI, else `hack projects --json --no-daemon`; native rows add `HACK_RUNTIME_BACKEND=native hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | | Services CPU/memory | `hack usage --json --details` (until R3) | 5 s while visible | 10 s | | Logs | `hack logs --json --path [] --tail 500` (one long-running child) | stream | killed on tab change | | Env | `hack env list --json --path ` | on tab open, after env actions | 10 s | From acbf11183efb1520ca2b1e4eb65713e6ad5d1dc1 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:10:09 -0500 Subject: [PATCH 4/8] docs(desktop): word the request list as contract requests Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 8da6b2c37..691f9ed36 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -283,7 +283,7 @@ killed with its process group on expiry; a timeout makes the screen Unknown. | Upgrades | R14 daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | | Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | 10 min for `up`, 2 min others | -### Requests to the CLI and daemon (one PR each, for other sessions) +### Requests to the CLI and daemon (each small enough for one PR) 1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. 2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. From 4e6f5479ae74474d0004d0d2b6f2dbaabce27755 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:11:45 -0500 Subject: [PATCH 5/8] docs(desktop): address design review round 2 (exit-0 role, backend-split checks, upgrade evidence) Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 107 +++++++++++++++--------- apps/desktop/diagrams/data-flow.mmd | 2 +- apps/desktop/diagrams/data-flow.svg | 4 +- apps/desktop/diagrams/project-state.mmd | 7 +- apps/desktop/diagrams/project-state.svg | 4 +- apps/desktop/diagrams/upgrade.mmd | 12 ++- apps/desktop/diagrams/upgrade.svg | 4 +- 7 files changed, 87 insertions(+), 53 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 691f9ed36..84a06d038 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -17,7 +17,7 @@ second implementation of them. | Reads observe only | A poll never starts Docker, hackd, a VM or anything else. A command that can start something (today: `hack global status`) is not polled until it has an observe-only mode (R13). | | No app state beyond window layout | Selected project, window size, sidebar width, theme override. Nothing else is stored. | | No credentials, no passwords | No password field, no `sudo` from the app. Admin steps go to the user's terminal ([Onboarding](#2-first-run-onboarding)). | -| Hack's secret values never appear | Env shows names and layers only, never a reveal. Logs are service output, shown as the CLI emits them, with a warning, never written to disk and with no copy-all (owner decision: the Logs tab is in scope). | +| No env-value reads or reveals | Env shows names and layers only, never a reveal. Exception: logs are unredacted service output (which can include values Hack injected), shown as the CLI emits them, with a warning, never written to disk and with no copy-all (owner decision: the Logs tab is in scope). | | Unknown is not absent | A failed or timed-out read shows "could not read" with the error, never "stopped" or "0 projects". | | Small | Target under 4,000 lines of Rust for v0.1. The old `apps/macos` grew to 13,800 lines through an embedded terminal, command palette, editor and agent integrations, tickets and remote features. None of those come back. | @@ -44,7 +44,7 @@ detail pane, status bar. Light and dark follow the system | Empty | Registry has no projects | "No projects yet" + `cd your-repo && hack init` | Copy command, open docs | | Daemon not running | `/v1/status` unreachable | List loads through the CLI; banner "hackd is off: status refreshes slowly" | Start hackd | | Daemon other version | `/v1/status.version` ≠ CLI version | CLI fallback; banner "hackd vX, CLI vY" | Restart hackd | -| Engine not running | `runtime_ok: false` | Rows keep names, dots "?"; banner with `runtime_error` | Open engine help | +| Docker not reachable | `runtime_ok: false` (Docker observation only) | Compose rows keep names, dots "?"; native rows keep their own state from native `ps`; banner with `runtime_error` | Open Docker help | | CLI missing or too old | `hack` not on PATH, or outside the supported range | Full-pane card instead of the list; status bar shows no engine/hackd state | Copy install command | | Error | CLI exit ≠ 0 or bad JSON | Banner with `code` and `message`; last good list kept, marked stale with its age | Retry, copy error | @@ -59,9 +59,9 @@ Project views read legacy inputs; a canonical native project comes back with `input_diagnostic`. The app recognises that code and asks `hack ps` instead rather than showing "Needs attention". Every command for a native project (`ps`, `up`, `down`, `restart`, `logs`) runs with `HACK_RUNTIME_BACKEND=native`, which is how the CLI -selects that backend today. Native `data.status` maps as: +selects that backend today. Docker's `runtime_ok` never changes a native row. Native `data.status` maps as: `not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded -from `items[].state`, `runtime_degraded` → Degraded, `runtime_not_running` and +from `items[].state`, `items[].health` and `items[].readiness`, `runtime_degraded` → Degraded, `runtime_not_running` and `runtime_busy` and `owner_unconfirmed` → Unknown (with the status as reason), `provider_pin_changed` → Needs attention. @@ -77,7 +77,8 @@ Header: name, state pill (+ uptime when known), backend pill, URL, buttons, | Unknown | runtime unreadable, read failed, or an action timed out or was interrupted | grey "Status unknown" + reason | Start (disabled while engine down) | | Stopped | no running container and no failed one | grey "Stopped" | Start | | Starting | this app's `up` in flight, or native `pending` | blue "Starting…" | none (no cancel in v0) | -| Running | every long-running service has a running container; one-shot services that exited 0 count as completed | green "Running · 2 hours" (from Docker status text) | Open, Restart, Stop | +| Running | every defined service has a running container | green "Running · 2 hours" (from Docker status text) | Open, Restart, Stop | +| Running, some exited (0) | the rest run; some exited 0 and their completion role is unknown | green "Running · migrate exited (0)" with tooltip "completion role unknown" | Open, Restart, Stop | | Degraded | some running; a long-running service down, exited ≠ 0, or `(unhealthy)` | amber "Degraded · worker down" | Open, Restart, Stop | | Stopping | this app's `down` in flight | grey "Stopping…" | none | | Failed start | last `up` reported `services.failed`, or nothing runs and a service exited ≠ 0 | red "Failed to start: api, worker" | Start (retry), Stop, logs filtered to those services | @@ -92,9 +93,10 @@ Rules: failed: : " under the header. - An action that times out or loses its process → Unknown until the next read. - Exit codes and uptime come from Docker's status text (`Exited (1) …`, `Up 2 hours`); - structured fields are request R2. A one-shot service is one with `restart: "no"` - semantics as Hack's startup state reports it; until R2 exposes that, an exit 0 - is treated as completed, never as down. + structured fields are request R2. Hack decides completion from a known role, + not exit 0 alone; until R2 exposes that role, an exit-0 service is shown as + "exited (0), completion role unknown": not down, and not claimed as fully running. +- External changes (a `hack up` in a terminal) show up on the next read. ### Services tab @@ -118,7 +120,9 @@ Source: `hack logs --json --path --tail 500` (ndjson, follows by default) | Very long lines | Cut at 4 KB on screen with "… (+N KB)", click to expand; `entry.truncated` shown as "truncated by hack". | | Secrets warning | Fixed note: "Logs are shown as your services print them. Hack does not hide secrets in logs." No copy-all; buffer never written to disk; "Open in terminal" offered. | | Finished | `end` event on a `--no-follow` read: "End of log" (normal). | -| Stream error | `error` event or exit ≠ 0: banner with reason and Reconnect. Engine down: "Engine not running". | +| Follow ended | while following, an `end` event (`eof` from Compose, `closed` from Loki) and exit 0: "Log stream ended" + Reconnect. Not an error. | +| Stream error | `error` event or exit ≠ 0: banner with reason and Reconnect. Docker down: "Docker not reachable". | +| Closed by the app | tab change, project change or pause timeout: the app kills the stream; nothing is shown. | | Stopped project | Last 200 lines (`--no-follow`), marked "project stopped". | ### Env tab (names only, never values) @@ -163,8 +167,10 @@ check, in order. Every check is read-only; every card offers **Check again**. | CLI | `hack --version` (text; R1 for JSON) | `brew install hack-dance/tap/hack` (copy) | no | Cannot skip | | Engine | `runtime_ok` from `/v1/projects` or `hack projects --json` | open Docker Desktop / engine docs; native setup command (HACK-2193) | no | "Engine not running" on every project | | hackd | `hack daemon status --json` | `hack daemon start` | no | Status bar "hackd off"; slower refresh | -| Routing and DNS | macOS: doctor `dnsmasq:53` and `coredns forwarding`; native: `native host dns`. Linux: **unknown** until R13 (only `hack global status`, which can start Docker, reports it) | `hack global install` | **yes** | URLs greyed "DNS not set up"; "unknown" when no check reports | -| Certificate trust | **Unknown** today for Compose: doctor `caddy local ca` checks the exported CA, not system or browser trust (R16). Native: `native root trust` | `hack global trust` | **yes** | Lock icon on URLs: "browser may warn" | +| Routing and DNS (Compose) | macOS: doctor `resolver:`, `dnsmasq.conf:`, `dnsmasq:53` and `coredns forwarding` (all must pass). Linux: doctor `linux dns route`. Never `hack global status` (can start Docker) | `hack global install` | **yes** | URLs greyed "DNS not set up"; "unknown" when a check is missing | +| Routing and DNS (native) | `hack doctor --browser-url ` → `native host dns` (absent without `--browser-url`) | none from the app: `hack global install` refuses native; link to the native setup doc (HACK-2193) | — | "unknown" | +| Certificate trust (Compose) | **Unknown** today: doctor `caddy local ca` checks the exported CA, not system or browser trust (R16) | `hack global trust` | **yes** | Lock icon on URLs: "browser may warn" | +| Certificate trust (native) | doctor `native root trust` | none from the app: `hack global trust` refuses native; doc link | — | "unknown" | | Login service | `daemon status` `service.installed` | `hack daemon install` | no | Always-on switch disabled | Skips are not stored: the next check finds the step still open and Doctor lists @@ -210,12 +216,13 @@ with only Copy and Open doc. No Run button appears without a `fix.kind: safe`. |---|---|---|---| | Update available | R14 (read-only, any install kind). `hack update --check` rejects Homebrew installs and candidate launchers, so it is not used | Chip "Hack vX available" | Review | | Checking | `hack upgrade check --json` (#790) | Spinner, then plan | none | -| Blocked | `ready: false`. `blockers[]` / `incomplete[]` are `{scope,id,summary}`; the command or action comes from the matching finding (`machine[]` or `projects[].findings[]` by scope + id) | One card per blocker | Copy / Open Terminal; Run only with R7a's `kind: safe`; Re-check | +| Blocked | `ready: false`. `blockers[]` / `incomplete[]` are `{scope,id,summary}`; the command or action comes from the matching finding (`machine[]` or `projects[].findings[]` by scope + id) | One card per blocker | Copy / Open Terminal; Run only with R17's `kind: safe`; Re-check | | Ready | `ready: true`, `plan[]` (`command` / `args` / `action`, `blocker`) | Ordered plan | Upgrade | | Snapshotting | `hack upgrade snapshot --json` | Progress | none | | Snapshot incomplete | `complete: false` or `gaps[]` | Gaps listed; Upgrade stays disabled | Re-check | -| Applying | Homebrew: `brew upgrade hack` **and** `brew upgrade --cask hack-desktop`, in a terminal, then plan steps | Steps with ✓/✗; partial success named ("CLI upgraded, app not") | none | -| Restarting daemon | `hack daemon restart` (exists), then `/v1/status.version` == CLI version. Doing this automatically after upgrade is HACK-2216 (proposed) | "Restarting hackd…" | none | +| Applying | Homebrew: `brew upgrade hack` **and** `brew upgrade --cask hack-desktop`, in a terminal, then plan steps | Each step ✓ only on fresh evidence: CLI = `hack --version` equals the target; app = the cask's installed version (`brew info --json=v2 --cask hack-desktop`) equals the target; a plan step = `hack upgrade check --json` no longer lists it. No evidence → that step stays Unknown (user confirmation or window focus is not evidence) | Check again | +| Restarting daemon | `hack daemon restart` (exists), then `/v1/status.version` == CLI version. Doing this automatically after upgrade is HACK-2216 (proposed) | "Restarting hackd…"; a mismatch after restart → Failed | none | +| Done | `ready: true` with an empty `plan[]`, CLI and app at the target, hackd version matches | "Hack vX is up to date" | none | | Failed | a step exit ≠ 0, or versions still differ | Which step and its error; "Files were saved" only when a complete snapshot exists | Roll back (only with a complete snapshot), Copy error | | Unknown | terminal step interrupted or not confirmed | "Could not confirm the upgrade" | Re-check | | Rolling back | `hack upgrade rollback --rollback-manifest --json` | Restored / not restored (`kept_as`, reasons) | none | @@ -271,6 +278,18 @@ Fields and commands needed from the CLI side: Polling stops while the window is hidden. Every CLI call has a timeout and is killed with its process group on expiry; a timeout makes the screen Unknown. +Exit codes: a valid JSON payload is parsed even when the exit code is not 0, +because the unhealthy states live there. + +| Read | Output | Non-zero exit means | +|---|---|---| +| `hack doctor --json` | envelope `{ok:true,data}` | exit 1 = some check is `error` (still a report) | +| `hack env list --json` | bare object | exit 1 = a required key is missing (still a report) | +| `hack upgrade check --json` (#790) | bare report | exit 1 = not `ready` (still a report) | +| `hack up\|down\|restart\|open --json`, native `ps --json` | envelope; `{ok:false,error}` on failure | failure, with `error.code` | +| `hack projects --json`, `daemon status --json`, `usage --json` | bare object | failure (no report) | +| `hack logs --json` | ndjson events | see Logs | + | Screen | Reads | How often | Timeout | |---|---|---|---| | Sidebar, header | `GET /v1/projects` if `/v1/status.version` matches the CLI, else `hack projects --json --no-daemon`; native rows add `HACK_RUNTIME_BACKEND=native hack ps --json --path` | 2 s with hackd, 10 s without; and after every action | 3 s / 15 s | @@ -283,30 +302,8 @@ killed with its process group on expiry; a timeout makes the screen Unknown. | Upgrades | R14 daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | | Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | 10 min for `up`, 2 min others | -### Requests to the CLI and daemon (each small enough for one PR) - -1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. -2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. -3. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. -4. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. -5. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`). -6. **R5b** Activity reader: `hack activity --json [--project] [--limit]`. -7. **R6** Action progress: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` before the final envelope. -8. **R7a** Doctor metadata: per check `area`, `scope: machine | project`, `fix: {kind: safe | admin | manual, args[], doc_url}` (same shape as #790's findings plus `kind`). -9. **R7b** One fix without prompts: `hack doctor --fix --only --yes --json`, only for `kind: safe`. -10. **R7c** Machine-only doctor: `hack doctor --machine --json` (no project), so the app does not need to de-duplicate. -11. **R8** Daemon unit staleness: `service.unit_stale` and reason in `hack daemon status --json`, and `version` there. -12. **R9** (optional) Push updates: `GET /v1/projects?wait=` long-poll. -13. **R10** Env key state: `key_state: ok | missing | unreadable` in `hack env list --json` (no values). -14. **R11** Admin argv plan: `hack global install|trust --plan --json` listing the root-only argv lines. -15. **R12a** Always-on persistence and `hack autostart add|remove|list|pause|resume` ([section 6](#6-always-on-hack-2213-proposed-contract)). -16. **R12b** Always-on fields in project payloads and `autostart_global`. -17. **R12c** Login run: hackd starts always-on projects at service start, retries up to `max_failures`, and honours pause. -18. **R13** Observe-only `hack global status --json --observe` that never starts Docker; reports `host_dns_ok` on both platforms. -19. **R14** Read-only update check for any install kind: `hack update --check --json --observe` → `{current, latest, available, install_kind, upgrade_command}`. -20. **R15** `--json` for `hack daemon start|stop|restart|install`. -21. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. -22. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. +The 22 numbered requests (R1–R17) this design needs from the CLI and daemon are in +[Appendix C](#appendix-c-requests-to-the-cli-and-daemon). ## 8. Scope @@ -355,6 +352,34 @@ A macOS system prompt could come later for single root-only lines from R11, neve 1. Build a universal `.app` (`aarch64` + `x86_64`) with `Info.plist` (`dance.hack.desktop`, minimum 15.0). 2. `codesign --options runtime --timestamp` with the Developer ID Application identity from CI secrets. -3. Zip, `xcrun notarytool submit --wait` with the notary API key from CI secrets, `xcrun stapler staple`, `stapler validate`. -4. Upload the zip to the release; update the cask's `version` and `sha256` in the tap (as the formula job does today). -5. A separate, manual workflow does this until the lead folds it into the release. +3. Zip a submission copy, `xcrun notarytool submit --wait` with the notary API key from CI secrets. +4. `xcrun stapler staple` the `.app`, `stapler validate`, then create the **distribution** zip from the stapled app. +5. Upload that zip; compute the cask `sha256` from it and update the cask in the tap (as the formula job does today). +6. A separate, manual workflow does this until the lead folds it into the release. + +## Appendix C: requests to the CLI and daemon + +Each is small enough for one PR. + +1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. +2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. +3. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. +4. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. +5. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`). +6. **R5b** Activity reader: `hack activity --json [--project] [--limit]`. +7. **R6** Action progress: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` before the final envelope. +8. **R7a** Doctor metadata (a new doctor schema): per check `area`, `scope: machine | project`, `fix: {kind: safe | admin | manual, args[], doc_url}`. +9. **R7b** One fix without prompts: `hack doctor --fix --only --yes --json`, only for `kind: safe`. +10. **R7c** Machine-only doctor: `hack doctor --machine --json` (no project), so the app does not need to de-duplicate. +11. **R8** Daemon unit staleness: `service.unit_stale` and reason in `hack daemon status --json`, and `version` there. +12. **R9** (optional) Push updates: `GET /v1/projects?wait=` long-poll. +13. **R10** Env key state: `key_state: ok | missing | unreadable` in `hack env list --json` (no values). +14. **R11** Admin argv plan: `hack global install|trust --plan --json` listing the root-only argv lines. +15. **R12a** Always-on persistence and `hack autostart add|remove|list|pause|resume` ([section 6](#6-always-on-hack-2213-proposed-contract)). +16. **R12b** Always-on fields in project payloads and `autostart_global`. +17. **R12c** Login run: hackd starts always-on projects at service start, retries up to `max_failures`, and honours pause. +18. **R13** Observe-only `hack global status --json --observe` that never starts Docker; reports `host_dns_ok` on both platforms. +19. **R14** Read-only update check for any install kind: `hack update --check --json --observe` → `{current, latest, available, install_kind, upgrade_command}`. +20. **R15** `--json` for `hack daemon start|stop|restart|install`. +21. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. +22. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. diff --git a/apps/desktop/diagrams/data-flow.mmd b/apps/desktop/diagrams/data-flow.mmd index 89822f7e7..40737e0c6 100644 --- a/apps/desktop/diagrams/data-flow.mmd +++ b/apps/desktop/diagrams/data-flow.mmd @@ -6,7 +6,7 @@ flowchart LR App -->|"GET /v1/status, /v1/projects"| D["hackd socket"] CLI --> D CLI --> E["Engine (Docker or native VM)"] - D -->|"events + 30s inspect"| E + D -->|"Docker events + 30s inspect (Docker only)"| E App -.->|"opens"| T["Terminal (admin steps)"] App -.->|"opens"| B["Browser (project URL)"] classDef live stroke-width:1.6px diff --git a/apps/desktop/diagrams/data-flow.svg b/apps/desktop/diagrams/data-flow.svg index 36aadf201..af8309432 100644 --- a/apps/desktop/diagrams/data-flow.svg +++ b/apps/desktop/diagrams/data-flow.svg @@ -1,4 +1,4 @@ -Desktop app · where data comes fromhack … --json (as the user)GET /v1/status, /v1/projectsevents + 30s inspectopensopensHack desktop apphack CLIhackd socketEngine (Docker or nativeVM)Terminal (admin steps)Browser (project URL) \ No newline at end of file +]]>Desktop app · where data comes fromhack … --json (as the user)GET /v1/status, /v1/projectsDocker events + 30s inspect(Docker only)opensopensHack desktop apphack CLIhackd socketEngine (Docker or nativeVM)Terminal (admin steps)Browser (project URL) \ No newline at end of file diff --git a/apps/desktop/diagrams/project-state.mmd b/apps/desktop/diagrams/project-state.mmd index 45f1ac78c..6a0235a54 100644 --- a/apps/desktop/diagrams/project-state.mmd +++ b/apps/desktop/diagrams/project-state.mmd @@ -8,15 +8,20 @@ stateDiagram-v2 Observed --> Unknown : observation failed state Observed { Stopped --> Starting : Start - Starting --> Running : all ready + Stopped --> Running : started elsewhere + Starting --> Running : all running Starting --> FailedStart : a service failed Running --> Degraded : a service down Degraded --> Running : service back Running --> Stopping : Stop Degraded --> Stopping : Stop + FailedStart --> Stopping : Stop + Running --> Stopped : stopped elsewhere Stopping --> Stopped : observed stopped FailedStart --> Starting : Retry FailedStart --> NeedsAttention : always-on gave up + NeedsAttention --> Starting : Retry + NeedsAttention --> Stopped : cause fixed } note right of Unknown INV a failed or timed-out diff --git a/apps/desktop/diagrams/project-state.svg b/apps/desktop/diagrams/project-state.svg index 19d2c035b..fa5e14d4c 100644 --- a/apps/desktop/diagrams/project-state.svg +++ b/apps/desktop/diagrams/project-state.svg @@ -1,4 +1,4 @@ -Project detail · display statesObservedobservation okobservation failedStartall readya service faileda service downservice backStopStopobserved stoppedRetryalways-on gave upStoppedStartingRunningFailedStartDegradedStoppingNeedsAttentionUnknownINV a failed or timed-outread never shows Stopped \ No newline at end of file +]]>Project detail · display statesObservedobservation okobservation failedStartstarted elsewhereall runninga service faileda service downservice backStopStopStopstopped elsewhereobserved stoppedRetryalways-on gave upRetrycause fixedStoppedStartingRunningFailedStartDegradedStoppingNeedsAttentionUnknownINV a failed or timed-outread never shows Stopped \ No newline at end of file diff --git a/apps/desktop/diagrams/upgrade.mmd b/apps/desktop/diagrams/upgrade.mmd index 630e0355b..6cef90c08 100644 --- a/apps/desktop/diagrams/upgrade.mmd +++ b/apps/desktop/diagrams/upgrade.mmd @@ -4,16 +4,18 @@ title: Upgrade · app view of the flow stateDiagram-v2 direction TB [*] --> Checking + Checking --> Done : nothing remains Checking --> Blocked : blockers or incomplete - Checking --> Ready : ready + Checking --> Ready : work remains Blocked --> Checking : Re-check Ready --> Snapshotting : Upgrade Snapshotting --> Blocked : gaps or refused Snapshotting --> Applying : complete - Applying --> Unknown : interrupted + Applying --> Unknown : no evidence Applying --> Failed : step failed - Applying --> RestartingDaemon : steps done - RestartingDaemon --> Checking : verify + Applying --> RestartingDaemon : steps verified + RestartingDaemon --> Checking : versions match + RestartingDaemon --> Failed : mismatch Failed --> RollingBack : Roll back RollingBack --> Restored : files restored RollingBack --> Failed : partial restore @@ -23,7 +25,9 @@ stateDiagram-v2 INV rollback restores files only, never versions or containers end note + classDef ok stroke-width:1.6px classDef wait stroke-dasharray:2 2 classDef fail stroke-width:2px + class Done ok class Snapshotting,Applying,RestartingDaemon,RollingBack wait class Blocked,Failed,Unknown fail diff --git a/apps/desktop/diagrams/upgrade.svg b/apps/desktop/diagrams/upgrade.svg index aee4eda1a..1d9b45798 100644 --- a/apps/desktop/diagrams/upgrade.svg +++ b/apps/desktop/diagrams/upgrade.svg @@ -1,4 +1,4 @@ -Upgrade · app view of the flowblockers or incompletereadyRe-checkUpgradegaps or refusedcompleteinterruptedstep failedsteps doneverifyRoll backfiles restoredpartial restorere-checkre-checkCheckingBlockedReadySnapshottingApplyingUnknownFailedRestartingDaemonRollingBackRestoredINV rollback restores files only,never versions or containers \ No newline at end of file +]]>Upgrade · app view of the flownothing remainsblockers or incompletework remainsRe-checkUpgradegaps or refusedcompleteno evidencestep failedsteps verifiedversions matchmismatchRoll backfiles restoredpartial restorere-checkre-checkCheckingDoneBlockedReadySnapshottingApplyingUnknownFailedRestartingDaemonRollingBackRestoredINV rollback restores files only,never versions or containers \ No newline at end of file From 1bbb9f6f1d169765001a936d07d7f1b8e2540a6f Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:17:14 -0500 Subject: [PATCH 6/8] docs(desktop): correct exit/envelope table, upgrade step evidence, native readiness, split engine onboarding Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 84a06d038..80cea39ea 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -61,7 +61,7 @@ rather than showing "Needs attention". Every command for a native project (`ps`, `down`, `restart`, `logs`) runs with `HACK_RUNTIME_BACKEND=native`, which is how the CLI selects that backend today. Docker's `runtime_ok` never changes a native row. Native `data.status` maps as: `not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded -from `items[].state`, `items[].health` and `items[].readiness`, `runtime_degraded` → Degraded, `runtime_not_running` and +from `items[].state` and `items[].health` (`readiness` is not in ordinary `ps` output; until R2 an exited service with exit 0 is "exited (0), role unknown" and any other exit is down), `runtime_degraded` → Degraded, `runtime_not_running` and `runtime_busy` and `owner_unconfirmed` → Unknown (with the status as reason), `provider_pin_changed` → Needs attention. @@ -165,7 +165,8 @@ check, in order. Every check is read-only; every card offers **Check again**. | Step | Read-only check | Fix | Admin | Skipped / unknown shows | |---|---|---|---|---| | CLI | `hack --version` (text; R1 for JSON) | `brew install hack-dance/tap/hack` (copy) | no | Cannot skip | -| Engine | `runtime_ok` from `/v1/projects` or `hack projects --json` | open Docker Desktop / engine docs; native setup command (HACK-2193) | no | "Engine not running" on every project | +| Docker (Compose projects) | `runtime_ok` from `/v1/projects` or `hack projects --json` (Docker only) | open Docker Desktop / Docker docs | no | "Docker not reachable" on Compose projects only | +| Native runtime (native projects) | native `ps` `data.status` per native project (`runtime_not_running` etc.) | native setup doc (HACK-2193) | no | that native project shows Unknown; Compose projects unaffected | | hackd | `hack daemon status --json` | `hack daemon start` | no | Status bar "hackd off"; slower refresh | | Routing and DNS (Compose) | macOS: doctor `resolver:`, `dnsmasq.conf:`, `dnsmasq:53` and `coredns forwarding` (all must pass). Linux: doctor `linux dns route`. Never `hack global status` (can start Docker) | `hack global install` | **yes** | URLs greyed "DNS not set up"; "unknown" when a check is missing | | Routing and DNS (native) | `hack doctor --browser-url ` → `native host dns` (absent without `--browser-url`) | none from the app: `hack global install` refuses native; link to the native setup doc (HACK-2193) | — | "unknown" | @@ -220,7 +221,7 @@ with only Copy and Open doc. No Run button appears without a `fix.kind: safe`. | Ready | `ready: true`, `plan[]` (`command` / `args` / `action`, `blocker`) | Ordered plan | Upgrade | | Snapshotting | `hack upgrade snapshot --json` | Progress | none | | Snapshot incomplete | `complete: false` or `gaps[]` | Gaps listed; Upgrade stays disabled | Re-check | -| Applying | Homebrew: `brew upgrade hack` **and** `brew upgrade --cask hack-desktop`, in a terminal, then plan steps | Each step ✓ only on fresh evidence: CLI = `hack --version` equals the target; app = the cask's installed version (`brew info --json=v2 --cask hack-desktop`) equals the target; a plan step = `hack upgrade check --json` no longer lists it. No evidence → that step stays Unknown (user confirmation or window focus is not evidence) | Check again | +| Applying | Homebrew: `brew upgrade hack` **and** `brew upgrade --cask hack-desktop`, in a terminal, then plan steps | Each step ✓ only on fresh evidence: CLI = `hack --version` equals the target; app = the cask's installed version (`brew info --json=v2 --cask hack-desktop`) equals the target; a plan step = the same scope was checked successfully (a `live` project, not `unreadable`, not in `incomplete[]`) and its finding is now `ok`. A step that merely disappears, or whose scope is missing, unreadable or unchecked, stays Unknown. No evidence → that step stays Unknown (user confirmation or window focus is not evidence) | Check again | | Restarting daemon | `hack daemon restart` (exists), then `/v1/status.version` == CLI version. Doing this automatically after upgrade is HACK-2216 (proposed) | "Restarting hackd…"; a mismatch after restart → Failed | none | | Done | `ready: true` with an empty `plan[]`, CLI and app at the target, hackd version matches | "Hack vX is up to date" | none | | Failed | a step exit ≠ 0, or versions still differ | Which step and its error; "Files were saved" only when a complete snapshot exists | Roll back (only with a complete snapshot), Copy error | @@ -285,9 +286,10 @@ because the unhealthy states live there. |---|---|---| | `hack doctor --json` | envelope `{ok:true,data}` | exit 1 = some check is `error` (still a report) | | `hack env list --json` | bare object | exit 1 = a required key is missing (still a report) | -| `hack upgrade check --json` (#790) | bare report | exit 1 = not `ready` (still a report) | +| `hack upgrade check --json` (#790) | envelope `{ok:true,data:report}` | exit 1 = not `ready` (still a report) | | `hack up\|down\|restart\|open --json`, native `ps --json` | envelope; `{ok:false,error}` on failure | failure, with `error.code` | -| `hack projects --json`, `daemon status --json`, `usage --json` | bare object | failure (no report) | +| `hack daemon status --json`, `hack usage --json` | bare object | exit 1 = unhealthy (daemon not running, usage unavailable): still a report, rendered as that state | +| `hack projects --json` | bare object | failure (no report) | | `hack logs --json` | ndjson events | see Logs | | Screen | Reads | How often | Timeout | From 49fe1bd4fa9b1d8ccf858a629539901b0bfea33d Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:22:50 -0500 Subject: [PATCH 7/8] docs(desktop): native projects are view-only in v0 (foreground lifecycle), add request R2b Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 54 +++++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index 80cea39ea..db03c98ee 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -53,13 +53,16 @@ detail pane, status bar. Light and dark follow the system | Backend | v0 | Source | |---|---|---| | Compose | Full detail | `/v1/projects` or `hack projects --json` (`runtime.services[].containers[]`) | -| Native (beta) | Row and state pill only; Services shows "native: details need R2" | `HACK_RUNTIME_BACKEND=native hack ps --json --path ` (`data.status`) | +| Native (beta) | State pill and services from native `ps`; no actions and no log tail in the app (see below) | `HACK_RUNTIME_BACKEND=native hack ps --json --path ` (`data.status`) | Project views read legacy inputs; a canonical native project comes back with `input_diagnostic`. The app recognises that code and asks `hack ps` instead -rather than showing "Needs attention". Every command for a native project (`ps`, `up`, -`down`, `restart`, `logs`) runs with `HACK_RUNTIME_BACKEND=native`, which is how the CLI -selects that backend today. Docker's `runtime_ok` never changes a native row. Native `data.status` maps as: +rather than showing "Needs attention". Every command for a native project runs with +`HACK_RUNTIME_BACKEND=native`, which is how the CLI selects that backend today. +Native `up` and `restart` stay in the foreground and refuse `--json` and +`--detach`, so v0 offers no native actions: the header shows the terminal +command instead (R2b below). Native finite logs are a one-service snapshot, +so the combined tail is replaced by a note until the Logs tab reads them per service. Docker's `runtime_ok` never changes a native row. Native `data.status` maps as: `not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded from `items[].state` and `items[].health` (`readiness` is not in ordinary `ps` output; until R2 an exited service with exit 0 is "exited (0), role unknown" and any other exit is down), `runtime_degraded` → Degraded, `runtime_not_running` and `runtime_busy` and `owner_unconfirmed` → Unknown (with the status as reason), @@ -304,7 +307,7 @@ because the unhealthy states live there. | Upgrades | R14 daily; `hack upgrade check --json` on Review | daily / on demand | 30 s / 60 s | | Actions | `hack up -d --json`, `down --json`, `restart --json`, `open --json`, `daemon start\|restart\|install` (exit code), `autostart …` | on click | 10 min for `up`, 2 min others | -The 22 numbered requests (R1–R17) this design needs from the CLI and daemon are in +The 23 numbered requests (R1–R17) this design needs from the CLI and daemon are in [Appendix C](#appendix-c-requests-to-the-cli-and-daemon). ## 8. Scope @@ -365,23 +368,24 @@ Each is small enough for one PR. 1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. 2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. -3. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. -4. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. -5. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`). -6. **R5b** Activity reader: `hack activity --json [--project] [--limit]`. -7. **R6** Action progress: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` before the final envelope. -8. **R7a** Doctor metadata (a new doctor schema): per check `area`, `scope: machine | project`, `fix: {kind: safe | admin | manual, args[], doc_url}`. -9. **R7b** One fix without prompts: `hack doctor --fix --only --yes --json`, only for `kind: safe`. -10. **R7c** Machine-only doctor: `hack doctor --machine --json` (no project), so the app does not need to de-duplicate. -11. **R8** Daemon unit staleness: `service.unit_stale` and reason in `hack daemon status --json`, and `version` there. -12. **R9** (optional) Push updates: `GET /v1/projects?wait=` long-poll. -13. **R10** Env key state: `key_state: ok | missing | unreadable` in `hack env list --json` (no values). -14. **R11** Admin argv plan: `hack global install|trust --plan --json` listing the root-only argv lines. -15. **R12a** Always-on persistence and `hack autostart add|remove|list|pause|resume` ([section 6](#6-always-on-hack-2213-proposed-contract)). -16. **R12b** Always-on fields in project payloads and `autostart_global`. -17. **R12c** Login run: hackd starts always-on projects at service start, retries up to `max_failures`, and honours pause. -18. **R13** Observe-only `hack global status --json --observe` that never starts Docker; reports `host_dns_ok` on both platforms. -19. **R14** Read-only update check for any install kind: `hack update --check --json --observe` → `{current, latest, available, install_kind, upgrade_command}`. -20. **R15** `--json` for `hack daemon start|stop|restart|install`. -21. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. -22. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. +3. **R2b** Native lifecycle the app can drive: a detached native `up`/`restart` with a `--json` result (or a hackd-owned start), so the app need not own a foreground process. +4. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. +5. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. +6. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`). +7. **R5b** Activity reader: `hack activity --json [--project] [--limit]`. +8. **R6** Action progress: `hack up|down|restart --json --progress` emitting ndjson `{type:"progress", service, phase}` before the final envelope. +9. **R7a** Doctor metadata (a new doctor schema): per check `area`, `scope: machine | project`, `fix: {kind: safe | admin | manual, args[], doc_url}`. +10. **R7b** One fix without prompts: `hack doctor --fix --only --yes --json`, only for `kind: safe`. +11. **R7c** Machine-only doctor: `hack doctor --machine --json` (no project), so the app does not need to de-duplicate. +12. **R8** Daemon unit staleness: `service.unit_stale` and reason in `hack daemon status --json`, and `version` there. +13. **R9** (optional) Push updates: `GET /v1/projects?wait=` long-poll. +14. **R10** Env key state: `key_state: ok | missing | unreadable` in `hack env list --json` (no values). +15. **R11** Admin argv plan: `hack global install|trust --plan --json` listing the root-only argv lines. +16. **R12a** Always-on persistence and `hack autostart add|remove|list|pause|resume` ([section 6](#6-always-on-hack-2213-proposed-contract)). +17. **R12b** Always-on fields in project payloads and `autostart_global`. +18. **R12c** Login run: hackd starts always-on projects at service start, retries up to `max_failures`, and honours pause. +19. **R13** Observe-only `hack global status --json --observe` that never starts Docker; reports `host_dns_ok` on both platforms. +20. **R14** Read-only update check for any install kind: `hack update --check --json --observe` → `{current, latest, available, install_kind, upgrade_command}`. +21. **R15** `--json` for `hack daemon start|stop|restart|install`. +22. **R16** Trust status: a doctor check that reports whether the system trust store (and NSS on Linux) trusts the local CA, or `unknown`. +23. **R17** `kind: safe | admin | manual` on #790's findings and plan steps, so the app knows which to offer as Run. From affad8f60c85c850659fd55fbefb18aa1ff3c9c6 Mon Sep 17 00:00:00 2001 From: Dimitri Kennedy <344842+roodboi@users.noreply.github.com> Date: Fri, 13 Nov 2026 19:25:36 -0500 Subject: [PATCH 8/8] docs(desktop): native up --json is already detached; narrow R2b to restart/down Co-Authored-By: Claude Opus 5.5 --- apps/desktop/DESIGN.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/apps/desktop/DESIGN.md b/apps/desktop/DESIGN.md index db03c98ee..5d1e14eff 100644 --- a/apps/desktop/DESIGN.md +++ b/apps/desktop/DESIGN.md @@ -59,9 +59,10 @@ Project views read legacy inputs; a canonical native project comes back with `input_diagnostic`. The app recognises that code and asks `hack ps` instead rather than showing "Needs attention". Every command for a native project runs with `HACK_RUNTIME_BACKEND=native`, which is how the CLI selects that backend today. -Native `up` and `restart` stay in the foreground and refuse `--json` and -`--detach`, so v0 offers no native actions: the header shows the terminal -command instead (R2b below). Native finite logs are a one-service snapshot, +Native `up --json` already starts a detached owner and returns a JSON +result, but native `restart` (and authored `down`/`restart`) stay in the +foreground and refuse `--json`. v0 keeps native projects view-only as a scope +choice: the header shows the terminal commands instead (R2b below). Native finite logs are a one-service snapshot, so the combined tail is replaced by a note until the Logs tab reads them per service. Docker's `runtime_ok` never changes a native row. Native `data.status` maps as: `not_started` → Stopped, `pending` → Starting, `observed` → Running or Degraded from `items[].state` and `items[].health` (`readiness` is not in ordinary `ps` output; until R2 an exited service with exit 0 is "exited (0), role unknown" and any other exit is down), `runtime_degraded` → Degraded, `runtime_not_running` and @@ -368,7 +369,7 @@ Each is small enough for one PR. 1. **R1** `hack version --json`: `{version, channel, install_kind, binary_path}`. 2. **R2** Backend-aware project state in `/v1/projects` and `hack projects --json` (full and summary rows): `backend`, `state` (`running | degraded | starting | stopping | stopped | failed | unknown`), `started_at`, and per service `{state, ready, completed, health, exit_code, started_at}`; native projects included. -3. **R2b** Native lifecycle the app can drive: a detached native `up`/`restart` with a `--json` result (or a hackd-owned start), so the app need not own a foreground process. +3. **R2b** Native lifecycle the app can drive: a `--json` contract for native `restart` and authored `down`/`restart`, and qualification of the existing detached `up --json` for app use. 4. **R3** Per-service usage: `hack usage --json --path --by-service` with `cpuPercent`, `memUsedBytes`. 5. **R4** Logs: document the `--json` ndjson as stable; `--services a,b` honoured by every backend; `--max-line-bytes ` setting `entry.truncated`. 6. **R5a** Activity writer: append-only `activity.jsonl` in the Hack home, one line per `up`/`down`/`restart`/autostart (`ts, project, branch, action, origin, outcome, failed_services, duration_ms`), `O_APPEND` writes under one line size limit, rotation at a size cap, `origin` from `HACK_ORIGIN` (the app sets `app`).